~launchpad-pqm/launchpad/devel

12293.1.11 by Curtis Hovey
Updated copyright.
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).
1665 by Canonical.com Patch Queue Manager
Changes to bugtask.txt to fix /malone/assigned, with a test, and also somewhat clean up bugtask.txt. r=stub
3
2286 by Canonical.com Patch Queue Manager
[r=salgado] implement DistroReleaseBugTargeting
4
"""IBugTask-related browser views."""
5
1102 by Canonical.com Patch Queue Manager
Lucille had some XXXs which should have been NOTEs
6
__metaclass__ = type
7
2071 by Canonical.com Patch Queue Manager
[trivial] add more __all__ statements, other minor tidyings-up
8
__all__ = [
5084.11.9 by Jonathan Knowles
Rearranging the __all__ list into alphabetical order.
9
    'BugListingBatchNavigator',
9570.15.19 by Gavin Panella
Make bugtarget-portlet-bugfilters-content.pt available via two views, one with stats, one without.
10
    'BugListingPortletInfoView',
11
    'BugListingPortletStatsView',
5084.11.9 by Jonathan Knowles
Rearranging the __all__ list into alphabetical order.
12
    'BugNominationsView',
12075.3.1 by Gavin Panella
New function, group_comments_with_activity().
13
    'BugsBugTaskSearchListingView',
9087.5.1 by Tom Berger
interim commit so that i can pull in the latest trunk
14
    'bugtarget_renderer',
1716.1.190 by Christian Reis
Merge from RF, again, this time for real
15
    'BugTargetTraversalMixin',
5084.11.9 by Jonathan Knowles
Rearranging the __all__ list into alphabetical order.
16
    'BugTargetView',
12075.3.1 by Gavin Panella
New function, group_comments_with_activity().
17
    'bugtask_heat_html',
18
    'BugTaskBreadcrumb',
2519 by Canonical.com Patch Queue Manager
r=BjornT, more actions portlets converted to menus, introduction of LaunchpadView.
19
    'BugTaskContextMenu',
4755.1.48 by Curtis Hovey
Changes per review. Anonther massive restructuring.
20
    'BugTaskCreateQuestionView',
2071 by Canonical.com Patch Queue Manager
[trivial] add more __all__ statements, other minor tidyings-up
21
    'BugTaskEditView',
5254.2.11 by Curtis Hovey
Added the expirable bugtaks view, extended the buglisting to offer date_last_updated,
22
    'BugTaskExpirableListingView',
6341.4.1 by Bjorn Tillenius
Add an image:badges formatter for BugTaskListingItem.
23
    'BugTaskListingItem',
5084.11.9 by Jonathan Knowles
Rearranging the __all__ list into alphabetical order.
24
    'BugTaskListingView',
25
    'BugTaskNavigation',
3283.3.1 by Brad Bollenbach
create a new branch for bzr integration, to avoid 3 hour merge time
26
    'BugTaskPortletView',
5474.3.21 by Edwin Grubbs
Fixed issues raised in the review
27
    'BugTaskPrivacyAdapter',
4755.1.48 by Curtis Hovey
Changes per review. Anonther massive restructuring.
28
    'BugTaskRemoveQuestionView',
12075.3.1 by Gavin Panella
New function, group_comments_with_activity().
29
    'BugTasksAndNominationsView',
5084.11.9 by Jonathan Knowles
Rearranging the __all__ list into alphabetical order.
30
    'BugTaskSearchListingView',
31
    'BugTaskSetNavigation',
3063.2.26 by Bjorn Tillenius
fix so that +viewstatus works.
32
    'BugTaskStatusView',
3614.1.68 by Brad Bollenbach
reapply MaloneReleaseManagement
33
    'BugTaskTableRowView',
5084.11.1 by Jonathan Knowles
Adding the skeleton 'BugTaskTextView' class: a view for a simple text page displaying information about a bug task.
34
    'BugTaskTextView',
2487 by Canonical.com Patch Queue Manager
[r=BjornT] Malone URL changes. Yep. Also: contextualize the bug page,
35
    'BugTaskView',
7675.554.4 by Tom Berger
Scale the value used for calculating the number of heat flames to display so that it produces a reasonable amount of flames for the hottest bugs.
36
    'calculate_heat_display',
5084.11.9 by Jonathan Knowles
Rearranging the __all__ list into alphabetical order.
37
    'get_buglisting_search_filter_url',
38
    'get_comments_for_bugtask',
3049.2.1 by Dafydd Harries
add bug text pages
39
    'get_sortorder_from_request',
6458.3.2 by Abel Deuring
implemented reviewer's comments
40
    'get_visible_comments',
12075.3.1 by Gavin Panella
New function, group_comments_with_activity().
41
    'NominationsReviewTableBatchNavigatorView',
42
    'TextualBugTaskSearchListingView',
3973.1.46 by Steve Alexander
add bugtask SOP for improved bugtask page heading
43
    ]
2511 by Canonical.com Patch Queue Manager
[trivial] Lint fixes all over the tree. Improve lint script to reduce clutter, and paved the way for a lintmerge.
44
3470.1.1 by Diogo Matsubara
Fix https://launchpad.net/products/malone/+bug/33978 (Advanced search page doesn't do any input validation)
45
import cgi
12412.1.1 by Robert Collins
Refactor lookups of target releases to do one getCurrentSourceReleases for all distros and one for all distroseries.
46
from collections import defaultdict
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
47
from datetime import (
48
    datetime,
49
    timedelta,
50
    )
13130.1.12 by Curtis Hovey
Sorted imports.
51
from itertools import groupby
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
52
from math import (
53
    floor,
54
    log,
55
    )
12075.3.14 by Gavin Panella
Move group_comments_with_activity() to lp.bugs.browser.bugcomment.
56
from operator import attrgetter
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
57
import re
13402.4.5 by Graham Binns
Tweaked according to Rob's requirements.
58
import transaction
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
59
import urllib
60
61
from lazr.delegates import delegates
62
from lazr.enum import (
63
    EnumeratedType,
64
    Item,
65
    )
66
from lazr.lifecycle.event import ObjectModifiedEvent
67
from lazr.lifecycle.snapshot import Snapshot
68
from lazr.restful.interface import copy_field
69
from lazr.restful.interfaces import (
70
    IFieldHTMLRenderer,
71
    IJSONRequestCache,
72
    IReference,
73
    IWebServiceClientRequest,
74
    )
75
from lazr.uri import URI
12075.3.7 by Gavin Panella
Import utc from pytz, not UTC.
76
from pytz import utc
8029.2.1 by Tom Berger
Insert a JSON representation of the available official tags into the bug page
77
from simplejson import dumps
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
78
from z3c.ptcompat import ViewPageTemplateFile
79
from zope import (
80
    component,
81
    formlib,
82
    )
3283.2.8 by Brad Bollenbach
ONE LINE TEST FIX
83
from zope.app.form import CustomWidgetFactory
4584.2.5 by Graham Binns
Implemented bac's review requests
84
from zope.app.form.browser.itemswidgets import RadioWidget
3811.2.3 by Bjorn Tillenius
initial go at making the search on the Bugs front page work.
85
from zope.app.form.interfaces import (
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
86
    IDisplayWidget,
87
    IInputWidget,
88
    InputErrors,
89
    WidgetsError,
90
    )
91
from zope.app.form.utility import (
92
    setUpWidget,
93
    setUpWidgets,
94
    )
9209.4.5 by Guilherme Salgado
Update all existing breadcrumb adapters to use Breadcrumb rather than BreadcrumbBuilder. Also rename them all
95
from zope.component import (
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
96
    ComponentLookupError,
97
    getAdapter,
98
    getMultiAdapter,
99
    getUtility,
100
    queryMultiAdapter,
101
    )
3554.1.48 by Brad Bollenbach
Fix bug 49598 (Unable to unsubscribe from private bug)
102
from zope.event import notify
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
103
from zope.interface import (
104
    implementer,
105
    implements,
106
    Interface,
107
    providedBy,
108
    )
3554.1.48 by Brad Bollenbach
Fix bug 49598 (Unable to unsubscribe from private bug)
109
from zope.schema import Choice
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
110
from zope.schema.interfaces import (
111
    IContextSourceBinder,
112
    IList,
113
    )
3554.1.48 by Brad Bollenbach
Fix bug 49598 (Unable to unsubscribe from private bug)
114
from zope.schema.vocabulary import (
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
115
    getVocabularyRegistry,
116
    SimpleVocabulary,
117
    )
9094.1.15 by Gavin Panella
Don't generate a breadcrumb for a bug that the user is not permitted to see.
118
from zope.security.interfaces import Unauthorized
12622.5.1 by Curtis Hovey
Always remove the bugtask milestone when retargeting the product.
119
from zope.security.proxy import (
120
    isinstance as zope_isinstance,
121
    removeSecurityProxy,
122
    )
8490.3.70 by Martin Albisetti
Address review comments
123
from zope.traversing.interfaces import IPathAdapter
7849.16.48 by Sidnei da Silva
- Merge trunk
124
3095.1.1 by Brad Bollenbach
move buglist batch size into the config files, to help us performance tune for listing more bugs on a page
125
from canonical.config import config
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
126
from canonical.launchpad import (
127
    _,
128
    helpers,
129
    )
130
from canonical.launchpad.browser.feeds import (
131
    BugTargetLatestBugsFeedLink,
132
    FeedsMixin,
133
    )
13130.1.12 by Curtis Hovey
Sorted imports.
134
from canonical.launchpad.interfaces.launchpad import IHasExternalBugTracker
8071.6.1 by Graham Binns
Added a basic implementation of BugActivityItem, which works for summaries only.
135
from canonical.launchpad.mailnotification import get_unified_diff
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
136
from canonical.launchpad.searchbuilder import (
137
    all,
138
    any,
139
    NULL,
140
    )
1716.1.190 by Christian Reis
Merge from RF, again, this time for real
141
from canonical.launchpad.webapp import (
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
142
    canonical_url,
143
    enabled_with_permission,
144
    GetitemNavigation,
145
    LaunchpadView,
146
    Link,
147
    Navigation,
148
    NavigationMenu,
149
    redirection,
150
    stepthrough,
151
    )
152
from canonical.launchpad.webapp.authorization import check_permission
153
from canonical.launchpad.webapp.batching import TableBatchNavigator
154
from canonical.launchpad.webapp.breadcrumb import Breadcrumb
155
from canonical.launchpad.webapp.interfaces import ILaunchBag
156
from canonical.launchpad.webapp.menu import structured
157
from canonical.lazr.interfaces import IObjectPrivacy
9389.9.13 by Graham Binns
Fixed page titles.
158
from canonical.lazr.utils import smartquote
9389.10.20 by Tom Berger
more test fixes
159
from lp.answers.interfaces.questiontarget import IQuestionTarget
11929.9.1 by Tim Penhey
Move launchpadform into lp.app.browser.
160
from lp.app.browser.launchpadform import (
161
    action,
162
    custom_widget,
163
    LaunchpadEditFormView,
164
    LaunchpadFormView,
165
    )
12268.3.18 by Tim Penhey
Move the widgets around.
166
from lp.app.browser.lazrjs import (
167
    TextAreaEditorWidget,
168
    TextLineEditorWidget,
169
    vocabulary_to_choice_edit_items,
170
    )
13023.6.5 by Henning Eggers
Obfuscate browser title.
171
from lp.app.browser.stringformatter import FormattersAPI
11664.3.3 by Edwin Grubbs
Don't display bugs or the Report-bug button if Launchpad doesn't know where it tracks bugs.
172
from lp.app.browser.tales import (
173
    ObjectImageDisplayAPI,
174
    PersonFormatterAPI,
175
    )
11411.7.8 by j.c.sackett
Added missing ServiceUsage import.
176
from lp.app.enums import ServiceUsage
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
177
from lp.app.errors import (
178
    NotFoundError,
179
    UnexpectedFormData,
180
    )
13130.1.6 by Curtis Hovey
Move ILaunchpadCelebrity to lp.app.
181
from lp.app.interfaces.launchpad import (
182
    ILaunchpadCelebrities,
183
    IServiceUsage,
184
    )
12442.2.9 by j.c.sackett
Ran import reformatter per review.
185
from lp.app.validators import LaunchpadValidationError
12293.1.10 by Curtis Hovey
Formatted imports.
186
from lp.app.widgets.itemswidgets import LabeledMultiCheckBoxWidget
187
from lp.app.widgets.project import ProjectScopeWidget
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
188
from lp.bugs.browser.bug import (
189
    BugContextMenu,
190
    BugTextView,
191
    BugViewMixin,
192
    )
12075.3.14 by Gavin Panella
Move group_comments_with_activity() to lp.bugs.browser.bugcomment.
193
from lp.bugs.browser.bugcomment import (
194
    build_comments_from_chunks,
195
    group_comments_with_activity,
196
    )
13130.1.12 by Curtis Hovey
Sorted imports.
197
from lp.bugs.browser.structuralsubscription import (
198
    expose_structural_subscription_data_to_js,
199
    )
12164.2.6 by Gavin Panella
Move canonical.widgets.bug to lp.bugs.browser.widgets.bug.
200
from lp.bugs.browser.widgets.bug import BugTagsWidget
12164.3.5 by Gavin Panella
Move lib/canonical/widgets/bugtask.py to lib/lp/bugs/browser/widgets/bugtask.py and adjust all imports, fix lint, etc.
201
from lp.bugs.browser.widgets.bugtask import (
202
    AssigneeDisplayWidget,
203
    BugTaskAssigneeWidget,
204
    BugTaskBugWatchWidget,
205
    BugTaskSourcePackageNameWidget,
206
    DBItemDisplayWidget,
207
    NewLineToSpacesWidget,
208
    NominationReviewActionWidget,
209
    )
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
210
from lp.bugs.interfaces.bug import (
211
    IBug,
212
    IBugSet,
213
    )
214
from lp.bugs.interfaces.bugactivity import IBugActivity
8523.3.1 by Gavin Panella
Bugs tree reorg after automated migration.
215
from lp.bugs.interfaces.bugattachment import (
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
216
    BugAttachmentType,
217
    IBugAttachmentSet,
218
    )
8523.3.1 by Gavin Panella
Bugs tree reorg after automated migration.
219
from lp.bugs.interfaces.bugnomination import (
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
220
    BugNominationStatus,
221
    IBugNominationSet,
222
    )
8523.3.1 by Gavin Panella
Bugs tree reorg after automated migration.
223
from lp.bugs.interfaces.bugtask import (
12278.4.7 by Gavin Panella
Format imports.
224
    BugBlueprintSearch,
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
225
    BugBranchSearch,
226
    BugTagsSearchCombinator,
227
    BugTaskImportance,
228
    BugTaskSearchParams,
229
    BugTaskStatus,
230
    BugTaskStatusSearchDisplay,
231
    DEFAULT_SEARCH_BUGTASK_STATUSES_FOR_DISPLAY,
232
    IBugTask,
233
    IBugTaskSearch,
234
    IBugTaskSet,
235
    ICreateQuestionFromBugTaskForm,
236
    IFrontPageBugTaskSearch,
13506.4.2 by William Grant
Replace valid_upstreamtask with validate_target everywhere.
237
    IllegalTarget,
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
238
    INominationsReviewTableBatchNavigator,
239
    IPersonBugTaskSearch,
240
    IRemoveQuestionFromBugTaskForm,
241
    IUpstreamProductBugTaskSearch,
242
    UNRESOLVED_BUGTASK_STATUSES,
13402.4.5 by Graham Binns
Tweaked according to Rob's requirements.
243
    UserCannotEditBugTaskStatus,
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
244
    )
8523.3.1 by Gavin Panella
Bugs tree reorg after automated migration.
245
from lp.bugs.interfaces.bugtracker import BugTrackerType
10888.3.1 by Graham Binns
Moved BugWatch.getLastErrorMessage() into view code, where it actually belongs.
246
from lp.bugs.interfaces.bugwatch import BugWatchActivityStatus
8523.3.1 by Gavin Panella
Bugs tree reorg after automated migration.
247
from lp.bugs.interfaces.cve import ICveSet
7675.568.4 by Tom Berger
Don't use target_context for calculating bug heat if the context is IPerson or IMaloneApplication - they don't have a max_bug_heat attribute.
248
from lp.bugs.interfaces.malone import IMaloneApplication
13506.4.2 by William Grant
Replace valid_upstreamtask with validate_target everywhere.
249
from lp.bugs.model.bugtask import validate_target
12412.1.1 by Robert Collins
Refactor lookups of target releases to do one getCurrentSourceReleases for all distros and one for all distroseries.
250
from lp.registry.interfaces.distribution import (
251
    IDistribution,
252
    IDistributionSet,
253
    )
7675.110.3 by Curtis Hovey
Ran the migration script to move registry code to lp.registry.
254
from lp.registry.interfaces.distributionsourcepackage import (
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
255
    IDistributionSourcePackage,
256
    )
12412.1.1 by Robert Collins
Refactor lookups of target releases to do one getCurrentSourceReleases for all distros and one for all distroseries.
257
from lp.registry.interfaces.distroseries import (
258
    IDistroSeries,
259
    IDistroSeriesSet,
260
    )
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
261
from lp.registry.interfaces.person import (
262
    IPerson,
263
    IPersonSet,
264
    )
7675.110.3 by Curtis Hovey
Ran the migration script to move registry code to lp.registry.
265
from lp.registry.interfaces.product import IProduct
266
from lp.registry.interfaces.productseries import IProductSeries
10326.1.2 by Henning Eggers
Renamed project interfaces module to projectgroup.
267
from lp.registry.interfaces.projectgroup import IProjectGroup
7675.110.3 by Curtis Hovey
Ran the migration script to move registry code to lp.registry.
268
from lp.registry.interfaces.sourcepackage import ISourcePackage
13130.1.12 by Curtis Hovey
Sorted imports.
269
from lp.registry.model.personroles import PersonRoles
13506.9.1 by Curtis Hovey
Merged vocab contract fixes from dsp-picker-fields.
270
from lp.registry.vocabularies import MilestoneVocabulary
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
271
from lp.services.fields import PersonChoice
13130.1.12 by Curtis Hovey
Sorted imports.
272
from lp.services.propertycache import cachedproperty
12599.4.2 by Leonard Richardson
Merge from trunk.
273
274
275
DISPLAY_BUG_STATUS_FOR_PATCHES = {
12688.1.2 by Brad Crittenden
Fixed lint
276
    BugTaskStatus.NEW: True,
12599.4.2 by Leonard Richardson
Merge from trunk.
277
    BugTaskStatus.INCOMPLETE: True,
278
    BugTaskStatus.INVALID: False,
279
    BugTaskStatus.WONTFIX: False,
280
    BugTaskStatus.CONFIRMED: True,
281
    BugTaskStatus.TRIAGED: True,
282
    BugTaskStatus.INPROGRESS: True,
283
    BugTaskStatus.FIXCOMMITTED: True,
284
    BugTaskStatus.FIXRELEASED: False,
285
    BugTaskStatus.UNKNOWN: False,
12688.1.2 by Brad Crittenden
Fixed lint
286
    BugTaskStatus.EXPIRED: False,
12599.4.2 by Leonard Richardson
Merge from trunk.
287
    }
12620.1.6 by Robert Collins
Fix bugs-with-patches test - stop using an old view to test new data.
288
289
9087.5.1 by Tom Berger
interim commit so that i can pull in the latest trunk
290
@component.adapter(IBugTask, IReference, IWebServiceClientRequest)
291
@implementer(IFieldHTMLRenderer)
292
def bugtarget_renderer(context, field, request):
293
    """Render a bugtarget as a link."""
11626.3.10 by Curtis Hovey
Hush lints epic complaints about the changes files.
294
9087.5.1 by Tom Berger
interim commit so that i can pull in the latest trunk
295
    def render(value):
9087.5.3 by Tom Berger
another interim commit
296
        html = """<span>
297
          <a href="%(href)s" class="%(class)s">%(displayname)s</a>
298
        </span>""" % {
299
            'href': canonical_url(context.target),
300
            'class': ObjectImageDisplayAPI(context.target).sprite_css(),
9087.5.10 by Tom Berger
some changes post review
301
            'displayname': cgi.escape(context.bugtargetdisplayname)}
9087.5.3 by Tom Berger
another interim commit
302
        return html
8255.14.2 by Edwin Grubbs
Working delete and assign me buttons. Now using IFieldHTMLRenderer adapter.
303
    return render
304
11411.7.29 by j.c.sackett
Lint fixes.
305
3847.2.46 by Mark Shuttleworth
Test fixes with new date display
306
def unique_title(title):
307
    """Canonicalise a message title to help identify messages with new
308
    information in their titles.
309
    """
310
    if title is None:
311
        return None
3847.2.47 by Mark Shuttleworth
Add tests for new bug comment behaviour, and extra bug sample data.
312
    title = title.lower()
313
    if title.startswith('re:'):
314
        title = title[3:]
315
    return title.strip()
3847.2.46 by Mark Shuttleworth
Test fixes with new date display
316
7675.759.10 by Brad Crittenden
Fixed lint issues
317
12376.1.1 by Robert Collins
Refactoring to start getting slice info where we need it.
318
def get_comments_for_bugtask(bugtask, truncate=False, for_display=False,
12929.7.39 by j.c.sackett
Added spam controls to bug comments.
319
    slice_info=None, show_spam_controls=False):
3504.1.51 by kiko
r=bradb Refactor fetching of bug comments to issue O(1) queries. Essentially inverts the way we build the bug comments, fetching all the chunks first, grouping them by message. Should be a major performance boost for the bug page.
320
    """Return BugComments related to a bugtask.
321
322
    This code builds a sorted list of BugComments in one shot,
3847.2.46 by Mark Shuttleworth
Test fixes with new date display
323
    requiring only two database queries. It removes the titles
324
    for those comments which do not have a "new" subject line
12376.1.1 by Robert Collins
Refactoring to start getting slice info where we need it.
325
326
    :param for_display: If true, the zeroth comment is given an empty body so
327
        that it will be filtered by get_visible_comments.
12622.5.3 by Curtis Hovey
Hushed lint.
328
    :param slice_info: If not None, defines a list of slices of the comments
329
        to retrieve.
3504.1.51 by kiko
r=bradb Refactor fetching of bug comments to issue O(1) queries. Essentially inverts the way we build the bug comments, fetching all the chunks first, grouping them by message. Should be a major performance boost for the bug page.
330
    """
12376.1.2 by Robert Collins
Basic implementation in place, tests not updated.
331
    comments = build_comments_from_chunks(bugtask, truncate=truncate,
12929.7.39 by j.c.sackett
Added spam controls to bug comments.
332
        slice_info=slice_info, show_spam_controls=show_spam_controls)
12376.1.2 by Robert Collins
Basic implementation in place, tests not updated.
333
    # TODO: further fat can be shaved off here by limiting the attachments we
334
    # query to those that slice_info would include.
11456.1.3 by Robert Collins
Create a dedicated property for API use for bug attachments.
335
    for attachment in bugtask.bug.attachments_unpopulated:
3504.1.51 by kiko
r=bradb Refactor fetching of bug comments to issue O(1) queries. Essentially inverts the way we build the bug comments, fetching all the chunks first, grouping them by message. Should be a major performance boost for the bug page.
336
        message_id = attachment.message.id
337
        # All attachments are related to a message, so we can be
338
        # sure that the BugComment is already created.
12376.1.2 by Robert Collins
Basic implementation in place, tests not updated.
339
        if message_id not in comments:
340
            # We are not showing this message.
341
            break
10017.2.2 by Abel Deuring
added property BugComment.patches; removed attachment that are patches from BugComment.bugattachments; separate display of regular attachments and patches on bug pages
342
        if attachment.type == BugAttachmentType.PATCH:
343
            comments[message_id].patches.append(attachment)
344
        else:
345
            comments[message_id].bugattachments.append(attachment)
3504.1.51 by kiko
r=bradb Refactor fetching of bug comments to issue O(1) queries. Essentially inverts the way we build the bug comments, fetching all the chunks first, grouping them by message. Should be a major performance boost for the bug page.
346
    comments = sorted(comments.values(), key=attrgetter("index"))
3847.2.47 by Mark Shuttleworth
Add tests for new bug comment behaviour, and extra bug sample data.
347
    current_title = bugtask.bug.title
3847.2.46 by Mark Shuttleworth
Test fixes with new date display
348
    for comment in comments:
3847.2.47 by Mark Shuttleworth
Add tests for new bug comment behaviour, and extra bug sample data.
349
        if not ((unique_title(comment.title) == \
350
                 unique_title(current_title)) or \
351
                (unique_title(comment.title) == \
352
                 unique_title(bugtask.bug.title))):
353
            # this comment has a new title, so make that the rolling focus
354
            current_title = comment.title
355
            comment.display_title = True
13023.3.2 by William Grant
Lint.
356
    if for_display and comments and comments[0].index == 0:
12376.1.1 by Robert Collins
Refactoring to start getting slice info where we need it.
357
        # We show the text of the first comment as the bug description,
358
        # or via the special link "View original description", but we want
359
        # to display attachments filed together with the bug in the
360
        # comment list.
361
        comments[0].text_for_display = ''
3504.1.51 by kiko
r=bradb Refactor fetching of bug comments to issue O(1) queries. Essentially inverts the way we build the bug comments, fetching all the chunks first, grouping them by message. Should be a major performance boost for the bug page.
362
    return comments
363
364
12840.7.2 by j.c.sackett
Test pass, and we didn't have to throw in a ton of db lookups.
365
def get_visible_comments(comments, user=None):
5127.1.2 by Christian Reis
Fix for bug #139327, comments are missing in +text view. Also adds fields for privacy, security and dates created.
366
    """Return comments, filtering out empty or duplicated ones."""
367
    visible_comments = []
368
    previous_comment = None
369
    for comment in comments:
370
        # Omit comments that are identical to their previous
371
        # comment, which were probably produced by
372
        # double-submissions or user errors, and which don't add
373
        # anything useful to the bug itself.
374
        # Also omit comments with no body text or attachments to display.
375
        if (comment.isEmpty() or
376
            previous_comment and
377
            previous_comment.isIdenticalTo(comment)):
378
            continue
379
380
        visible_comments.append(comment)
381
        previous_comment = comment
382
6138.5.2 by Bjorn Tillenius
fill the ValidPersonOrTeamCache cache for all person's commenting on the bug.
383
    # These two lines are here to fill the ValidPersonOrTeamCache cache,
6138.5.3 by Bjorn Tillenius
typo.
384
    # so that checking owner.is_valid_person, when rendering the link,
12376.1.2 by Robert Collins
Basic implementation in place, tests not updated.
385
    # won't issue a DB query. Note that this should be obsolete now with
386
    # getMessagesForView improvements.
6138.5.2 by Bjorn Tillenius
fill the ValidPersonOrTeamCache cache for all person's commenting on the bug.
387
    commenters = set(comment.owner for comment in visible_comments)
388
    getUtility(IPersonSet).getValidPersons(commenters)
389
12840.7.2 by j.c.sackett
Test pass, and we didn't have to throw in a ton of db lookups.
390
    # If a user is supplied, we can also strip out comments that the user
391
    # cannot see, because they have been marked invisible.
12840.7.3 by j.c.sackett
Lint fixes.
392
    strip_invisible = True
12840.7.2 by j.c.sackett
Test pass, and we didn't have to throw in a ton of db lookups.
393
    if user is not None:
394
        role = PersonRoles(user)
12840.7.3 by j.c.sackett
Lint fixes.
395
        strip_invisible = not (role.in_admin or role.in_registry_experts)
396
    if strip_invisible:
397
        visible_comments = [c for c in visible_comments if c.visible]
12840.7.2 by j.c.sackett
Test pass, and we didn't have to throw in a ton of db lookups.
398
5127.1.2 by Christian Reis
Fix for bug #139327, comments are missing in +text view. Also adds fields for privacy, security and dates created.
399
    return visible_comments
400
401
2710 by Canonical.com Patch Queue Manager
[r=kiko] create a sortorder widget for the various bug listings that
402
def get_sortorder_from_request(request):
3270.3.49 by Matthew Paul Thomas
Fixes get_sortorder_from_request with code (and tests) from BjornT.
403
    """Get the sortorder from the request.
3554.1.31 by Brad Bollenbach
checkpoint
404
9678.4.41 by Guilherme Salgado
[r=barry] Change our tests that were using TestRequest to use LaunchpadTestRequest.
405
    >>> from canonical.launchpad.webapp.servers import LaunchpadTestRequest
406
    >>> get_sortorder_from_request(LaunchpadTestRequest(form={}))
3270.3.49 by Matthew Paul Thomas
Fixes get_sortorder_from_request with code (and tests) from BjornT.
407
    ['-importance']
9678.4.41 by Guilherme Salgado
[r=barry] Change our tests that were using TestRequest to use LaunchpadTestRequest.
408
    >>> get_sortorder_from_request(
409
    ...     LaunchpadTestRequest(form={'orderby': '-status'}))
3270.3.49 by Matthew Paul Thomas
Fixes get_sortorder_from_request with code (and tests) from BjornT.
410
    ['-status']
9678.4.41 by Guilherme Salgado
[r=barry] Change our tests that were using TestRequest to use LaunchpadTestRequest.
411
    >>> get_sortorder_from_request(LaunchpadTestRequest(
412
    ...     form={'orderby': 'status,-severity,importance'}))
3270.3.49 by Matthew Paul Thomas
Fixes get_sortorder_from_request with code (and tests) from BjornT.
413
    ['status', 'importance']
414
    >>> get_sortorder_from_request(
9678.4.41 by Guilherme Salgado
[r=barry] Change our tests that were using TestRequest to use LaunchpadTestRequest.
415
    ...     LaunchpadTestRequest(form={'orderby': 'priority,-severity'}))
3270.3.49 by Matthew Paul Thomas
Fixes get_sortorder_from_request with code (and tests) from BjornT.
416
    ['-importance']
417
    """
3270.3.48 by Matthew Paul Thomas
Fixes some tests.
418
    order_by_string = request.get("orderby", '')
3270.3.49 by Matthew Paul Thomas
Fixes get_sortorder_from_request with code (and tests) from BjornT.
419
    if order_by_string:
3554.1.31 by Brad Bollenbach
checkpoint
420
        if not zope_isinstance(order_by_string, list):
421
            order_by = order_by_string.split(',')
422
        else:
423
            order_by = order_by_string
3270.3.49 by Matthew Paul Thomas
Fixes get_sortorder_from_request with code (and tests) from BjornT.
424
    else:
425
        order_by = []
3270.3.48 by Matthew Paul Thomas
Fixes some tests.
426
    # Remove old order_by values that people might have in bookmarks.
427
    for old_order_by_column in ['priority', 'severity']:
428
        if old_order_by_column in order_by:
429
            order_by.remove(old_order_by_column)
430
        if '-' + old_order_by_column in order_by:
431
            order_by.remove('-' + old_order_by_column)
432
    if order_by:
433
        return order_by
2710 by Canonical.com Patch Queue Manager
[r=kiko] create a sortorder widget for the various bug listings that
434
    else:
435
        # No sort ordering specified, so use a reasonable default.
3270.3.10 by Matthew Paul Thomas
Changes BugTaskSeverity to BugTaskImportance.
436
        return ["-importance"]
2710 by Canonical.com Patch Queue Manager
[r=kiko] create a sortorder widget for the various bug listings that
437
2519 by Canonical.com Patch Queue Manager
r=BjornT, more actions portlets converted to menus, introduction of LaunchpadView.
438
9570.15.20 by Gavin Panella
Most callers of get_default_search_params() do not care about ordering.
439
def get_default_search_params(user):
9570.15.1 by Gavin Panella
Put the new bug stats stuff into the asynchronously loaded bugfilters portlet.
440
    """Return a BugTaskSearchParams instance with default values.
441
442
    By default, a search includes any bug that is unresolved and not a
443
    duplicate of another bug.
9570.15.20 by Gavin Panella
Most callers of get_default_search_params() do not care about ordering.
444
445
    If this search will be used to display a list of bugs to the user
446
    it may be a good idea to set the orderby attribute using
447
    get_sortorder_from_request():
448
9570.15.34 by Gavin Panella
The docstring in get_default_search_params() is not meant to be a valid doctest, but looks like one and is being run. Changed to not look like a doctest.
449
      params = get_default_search_params(user)
450
      params.orderby = get_sortorder_from_request(request)
9570.15.20 by Gavin Panella
Most callers of get_default_search_params() do not care about ordering.
451
9570.15.1 by Gavin Panella
Put the new bug stats stuff into the asynchronously loaded bugfilters portlet.
452
    """
9570.15.20 by Gavin Panella
Most callers of get_default_search_params() do not care about ordering.
453
    return BugTaskSearchParams(
9570.15.1 by Gavin Panella
Put the new bug stats stuff into the asynchronously loaded bugfilters portlet.
454
        user=user, status=any(*UNRESOLVED_BUGTASK_STATUSES), omit_dupes=True)
455
456
4318.3.6 by Gavin Panella
Add redirection functionality.
457
OLD_BUGTASK_STATUS_MAP = {
458
    'Unconfirmed': 'New',
459
    'Needs Info': 'Incomplete',
460
    'Rejected': 'Invalid',
461
    }
462
463
4318.3.19 by Gavin Panella
Address bug-workflow-2 review comments from thumper.
464
def rewrite_old_bugtask_status_query_string(query_string):
4318.3.22 by Gavin Panella
Address additional bug-workflow-2 review comments from thumper.
465
    """Return a query string with old status names replaced with new.
4318.3.6 by Gavin Panella
Add redirection functionality.
466
467
    If an old status string has been used in the query, construct a
4318.3.19 by Gavin Panella
Address bug-workflow-2 review comments from thumper.
468
    corrected query string for the search, else return the original
469
    query string.
4318.3.6 by Gavin Panella
Add redirection functionality.
470
    """
471
    query_elements = cgi.parse_qsl(
4318.3.19 by Gavin Panella
Address bug-workflow-2 review comments from thumper.
472
        query_string, keep_blank_values=True, strict_parsing=False)
4318.3.6 by Gavin Panella
Add redirection functionality.
473
    query_elements_mapped = []
474
475
    for name, value in query_elements:
476
        if name == 'field.status:list':
477
            value = OLD_BUGTASK_STATUS_MAP.get(value, value)
478
        query_elements_mapped.append((name, value))
479
480
    if query_elements == query_elements_mapped:
4318.3.19 by Gavin Panella
Address bug-workflow-2 review comments from thumper.
481
        return query_string
4318.3.6 by Gavin Panella
Add redirection functionality.
482
    else:
4318.3.19 by Gavin Panella
Address bug-workflow-2 review comments from thumper.
483
        return urllib.urlencode(query_elements_mapped, doseq=True)
4318.3.6 by Gavin Panella
Add redirection functionality.
484
485
5254.2.11 by Curtis Hovey
Added the expirable bugtaks view, extended the buglisting to offer date_last_updated,
486
def target_has_expirable_bugs_listing(target):
487
    """Return True or False if the target has the expirable-bugs listing.
488
489
    The target must be a Distribution, DistroSeries, Product, or
490
    ProductSeries, and the pillar must have enabled bug expiration.
491
    """
492
    if IDistribution.providedBy(target) or IProduct.providedBy(target):
493
        return target.enable_bug_expiration
494
    elif IProductSeries.providedBy(target):
495
        return target.product.enable_bug_expiration
496
    elif IDistroSeries.providedBy(target):
497
        return target.distribution.enable_bug_expiration
498
    else:
499
        # This context is not a supported bugtarget.
500
        return False
501
502
1716.1.190 by Christian Reis
Merge from RF, again, this time for real
503
class BugTargetTraversalMixin:
504
    """Mix-in in class that provides .../+bug/NNN traversal."""
505
506
    redirection('+bug', '+bugs')
507
508
    @stepthrough('+bug')
509
    def traverse_bug(self, name):
510
        """Traverses +bug portions of URLs"""
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
511
        return self._get_task_for_context(name)
1716.1.190 by Christian Reis
Merge from RF, again, this time for real
512
513
    def _get_task_for_context(self, name):
514
        """Return the IBugTask for this name in this context.
515
516
        If the bug has been reported, but not in this specific context, a
12599.4.2 by Leonard Richardson
Merge from trunk.
517
        redirect to the default context will be returned.
518
519
        Returns None if no bug with the given name is found, or the
520
        bug is not accessible to the current user.
1716.1.190 by Christian Reis
Merge from RF, again, this time for real
521
        """
522
        context = self.context
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
523
524
        # Raises NotFoundError if no bug is found
525
        bug = getUtility(IBugSet).getByNameOrID(name)
1716.1.190 by Christian Reis
Merge from RF, again, this time for real
526
12599.4.2 by Leonard Richardson
Merge from trunk.
527
        # Get out now if the user cannot view the bug. Continuing may
528
        # reveal information about its context
529
        if not check_permission('launchpad.View', bug):
530
            return None
531
1716.1.190 by Christian Reis
Merge from RF, again, this time for real
532
        # Loop through this bug's tasks to try and find the appropriate task
533
        # for this context. We always want to return a task, whether or not
534
        # the user has the permission to see it so that, for example, an
535
        # anonymous user is presented with a login screen at the correct URL,
536
        # rather than making it look as though this task was "not found",
537
        # because it was filtered out by privacy-aware code.
12482.1.1 by Robert Collins
Eager load related fields for bugs when executing bug.bugtasks.
538
        for bugtask in bug.bugtasks:
1716.1.190 by Christian Reis
Merge from RF, again, this time for real
539
            if bugtask.target == context:
3112.1.26 by Brad Bollenbach
Fix bug 33554 (Wrong "Forbidden" exception when viewing bug page)
540
                # Security proxy this object on the way out.
541
                return getUtility(IBugTaskSet).get(bugtask.id)
1716.1.190 by Christian Reis
Merge from RF, again, this time for real
542
12599.4.2 by Leonard Richardson
Merge from trunk.
543
        # If we've come this far, there's no task for the requested
544
        # context. Redirect to one that exists.
545
        return self.redirectSubTree(canonical_url(bug.default_bugtask))
1716.1.190 by Christian Reis
Merge from RF, again, this time for real
546
547
548
class BugTaskNavigation(Navigation):
4974.2.1 by Curtis Hovey
Fixes per pylint.
549
    """Navigation for the `IBugTask`."""
1716.1.190 by Christian Reis
Merge from RF, again, this time for real
550
    usedfor = IBugTask
551
552
    @stepthrough('attachments')
553
    def traverse_attachments(self, name):
4974.2.1 by Curtis Hovey
Fixes per pylint.
554
        """traverse to an attachment by id."""
1716.1.190 by Christian Reis
Merge from RF, again, this time for real
555
        if name.isdigit():
6887.5.6 by Gavin Panella
Change attachment edit link to use the canonical URL, and redirect all other possible places that an attachment can be seen over to the new canonical URL.
556
            attachment = getUtility(IBugAttachmentSet)[name]
557
            if attachment is not None and attachment.bug == self.context.bug:
13023.3.1 by William Grant
Use self.redirectSubTree() instead of redirection() whereever a subpath may be present.
558
                return self.redirectSubTree(
559
                    canonical_url(attachment), status=301)
1716.1.190 by Christian Reis
Merge from RF, again, this time for real
560
6887.5.14 by Gavin Panella
Use urldata to define the browser URL for a bug attachment.
561
    @stepthrough('+attachment')
562
    def traverse_attachment(self, name):
563
        """traverse to an attachment by id."""
564
        if name.isdigit():
565
            attachment = getUtility(IBugAttachmentSet)[name]
566
            if attachment is not None and attachment.bug == self.context.bug:
567
                return attachment
568
3607.4.2 by Bjorn Tillenius
make it possible to traverse to a bug comment. provide a simple view of a single comment.
569
    @stepthrough('comments')
570
    def traverse_comments(self, name):
4974.2.1 by Curtis Hovey
Fixes per pylint.
571
        """Traverse to a comment by id."""
3607.4.9 by Bjorn Tillenius
use BugComment instead of BugMessage.
572
        if not name.isdigit():
3607.4.17 by Bjorn Tillenius
review comments.
573
            return None
3607.4.9 by Bjorn Tillenius
use BugComment instead of BugMessage.
574
        index = int(name)
3504.1.51 by kiko
r=bradb Refactor fetching of bug comments to issue O(1) queries. Essentially inverts the way we build the bug comments, fetching all the chunks first, grouping them by message. Should be a major performance boost for the bug page.
575
        comments = get_comments_for_bugtask(self.context)
576
        # I couldn't find a way of using index to restrict the queries
577
        # in get_comments_for_bugtask in a way that wasn't horrible, and
578
        # it wouldn't really save us a lot in terms of database time, so
579
        # I have chosed to use this simple solution for now.
580
        #   -- kiko, 2006-07-11
3607.4.9 by Bjorn Tillenius
use BugComment instead of BugMessage.
581
        try:
10606.2.1 by Karl Fogel
Fix bug #546943 ("hide hidden comments when visited directly too"):
582
            comment = comments[index]
583
            if (comment.visible
584
                or check_permission('launchpad.Admin', self.context)):
585
                return comment
586
            else:
587
                return None
3607.4.9 by Bjorn Tillenius
use BugComment instead of BugMessage.
588
        except IndexError:
589
            return None
3614.1.50 by Brad Bollenbach
response to code review
590
3614.1.68 by Brad Bollenbach
reapply MaloneReleaseManagement
591
    @stepthrough('nominations')
592
    def traverse_nominations(self, nomination_id):
4974.2.1 by Curtis Hovey
Fixes per pylint.
593
        """Traverse to a nomination by id."""
3614.1.68 by Brad Bollenbach
reapply MaloneReleaseManagement
594
        if not nomination_id.isdigit():
595
            return None
596
        return getUtility(IBugNominationSet).get(nomination_id)
597
1716.1.190 by Christian Reis
Merge from RF, again, this time for real
598
    redirection('references', '..')
599
600
2630 by Canonical.com Patch Queue Manager
[trivial] lots of tidying up. converting all database classes to use NotFoundError consistently, and to import it from launchpad.interfaces in preparation for the move to a new zope3. Also, introduced a NameNotAvailable error. removed browser:traverse rdirective. commented out shipit test that fails sometimes.
601
class BugTaskSetNavigation(GetitemNavigation):
4974.2.1 by Curtis Hovey
Fixes per pylint.
602
    """Navigation for the `IbugTaskSet`."""
2628 by Canonical.com Patch Queue Manager
[trivial] converted a bunch of browser:traverse into navigation
603
    usedfor = IBugTaskSet
604
605
2560 by Canonical.com Patch Queue Manager
[r=BjornT] fix a regression in the Malone menus where the menus had
606
class BugTaskContextMenu(BugContextMenu):
4974.2.1 by Curtis Hovey
Fixes per pylint.
607
    """Context menu of actions that can be performed upon an `IBugTask`."""
2519 by Canonical.com Patch Queue Manager
r=BjornT, more actions portlets converted to menus, introduction of LaunchpadView.
608
    usedfor = IBugTask
609
610
5155.3.6 by Jonathan Knowles
Fixing merge.
611
class BugTaskTextView(LaunchpadView):
612
    """View for a simple text page displaying information about a bug task."""
613
614
    def render(self):
615
        """Return a text representation of the parent bug."""
616
        view = BugTextView(self.context.bug, self.request)
617
        view.initialize()
618
        return view.render()
619
620
11822.2.1 by Robert Collins
Remove the python code related to mentoring.
621
class BugTaskView(LaunchpadView, BugViewMixin, FeedsMixin):
4974.2.1 by Curtis Hovey
Fixes per pylint.
622
    """View class for presenting information about an `IBugTask`."""
2487 by Canonical.com Patch Queue Manager
[r=BjornT] Malone URL changes. Yep. Also: contextualize the bug page,
623
9389.9.13 by Graham Binns
Fixed page titles.
624
    override_title_breadcrumbs = True
625
2487 by Canonical.com Patch Queue Manager
[r=BjornT] Malone URL changes. Yep. Also: contextualize the bug page,
626
    def __init__(self, context, request):
3258.3.1 by Brad Bollenbach
move the bugtask edit page onto the bug page. needs serious mpt-love to fix very broken layout.
627
        LaunchpadView.__init__(self, context, request)
628
3691.436.10 by Mark Shuttleworth
Allow for the retraction of mentoring offers
629
        self.notices = []
630
2635 by Canonical.com Patch Queue Manager
[r=salgado] add a bugtarget search portlet
631
        # Make sure we always have the current bugtask.
632
        if not IBugTask.providedBy(context):
633
            self.context = getUtility(ILaunchBag).bugtask
634
        else:
635
            self.context = context
12482.1.4 by Robert Collins
Preload validity for the bug owner.
636
        list(getUtility(IPersonSet).getPrecachedPersonsFromIDs(
637
            [self.context.bug.ownerID], need_validity=True))
2635 by Canonical.com Patch Queue Manager
[r=salgado] add a bugtarget search portlet
638
9389.9.13 by Graham Binns
Fixed page titles.
639
    @property
640
    def page_title(self):
12599.4.2 by Leonard Richardson
Merge from trunk.
641
        heading = 'Bug #%s in %s' % (
642
            self.context.bug.id, self.context.bugtargetdisplayname)
13023.6.5 by Henning Eggers
Obfuscate browser title.
643
        title = FormattersAPI(self.context.bug.title).obfuscate_email()
644
        return smartquote('%s: "%s"') % (heading, title)
9389.9.13 by Graham Binns
Fixed page titles.
645
11477.2.9 by Brian Murray
redirect to +subscriptions if continuing with an unsubscribe request
646
    @property
647
    def next_url(self):
11477.2.12 by Brian Murray
ensure continue and cancel work regardless the domain
648
        """Provided so returning to the page they came from works."""
11477.2.15 by Brian Murray
check to ensure that referer is not localhost working around bug 98437
649
        referer = self.request.getHeader('referer')
650
651
        # XXX bdmurray 2010-09-30 bug=98437: work around zope's test
652
        # browser setting referer to localhost.
653
        if referer and referer != 'localhost':
654
            next_url = referer
11477.2.13 by Brian Murray
modify xx-bug-personal-subscriptions.txt to deal with the displayed bug number in the information message regarding subscription
655
        else:
656
            next_url = canonical_url(self.context)
657
        return next_url
11477.2.9 by Brian Murray
redirect to +subscriptions if continuing with an unsubscribe request
658
11477.2.12 by Brian Murray
ensure continue and cancel work regardless the domain
659
    @property
660
    def cancel_url(self):
661
        """Provided so returning to the page they came from works."""
11477.2.15 by Brian Murray
check to ensure that referer is not localhost working around bug 98437
662
        referer = self.request.getHeader('referer')
663
664
        # XXX bdmurray 2010-09-30 bug=98437: work around zope's test
665
        # browser setting referer to localhost.
666
        if referer and referer != 'localhost':
667
            cancel_url = referer
11477.2.13 by Brian Murray
modify xx-bug-personal-subscriptions.txt to deal with the displayed bug number in the information message regarding subscription
668
        else:
669
            cancel_url = canonical_url(self.context)
670
        return cancel_url
11477.2.12 by Brian Murray
ensure continue and cancel work regardless the domain
671
3498.7.1 by Bjorn Tillenius
checkpoint commit in order to merge in rf.
672
    def initialize(self):
3498.7.9 by Bjorn Tillenius
doctweaks.
673
        """Set up the needed widgets."""
3691.163.4 by Brad Bollenbach
checkpoint
674
        bug = self.context.bug
7437.4.6 by Leonard Richardson
Renamed ITemplateCache to IJSONRequestCache.
675
        IJSONRequestCache(self.request).objects['bug'] = bug
7437.4.1 by Leonard Richardson
Initial implementation.
676
3554.1.50 by Brad Bollenbach
more review response
677
        # See render() for how this flag is used.
3554.1.49 by Brad Bollenbach
review fixes
678
        self._redirecting_to_bug_list = False
3554.1.48 by Brad Bollenbach
Fix bug 49598 (Unable to unsubscribe from private bug)
679
7471.4.33 by Francis J. Lacoste
Renaming based on review.
680
        self.bug_title_edit_widget = TextLineEditorWidget(
12268.3.24 by Tim Penhey
Make the tag non-optional when specifying a TestLineEditorWidget.
681
            bug, IBug['title'], "Edit this summary", 'h1',
12268.3.14 by Tim Penhey
Allow the specifying of the edit_url.
682
            edit_url=canonical_url(self.context, view_name='+edit'))
7471.4.10 by Francis J. Lacoste
Make the bug title use the InlineTextLineEditorWidget.
683
11654.2.13 by Graham Binns
Updated XXX.
684
        # XXX 2010-10-05 gmb bug=655597:
12622.5.3 by Curtis Hovey
Hushed lint.
685
        # This line of code keeps the view's query count down,
686
        # possibly using witchcraft. It should be rewritten to be
687
        # useful or removed in favour of making other queries more
688
        # efficient. The witchcraft is because the subscribers are accessed
689
        # in the initial page load, so the data is actually used.
11654.2.14 by Graham Binns
Fixed more failing tests.
690
        if self.user is not None:
691
            list(bug.getSubscribersForPerson(self.user))
3554.1.48 by Brad Bollenbach
Fix bug 49598 (Unable to unsubscribe from private bug)
692
3498.7.1 by Bjorn Tillenius
checkpoint commit in order to merge in rf.
693
    def userIsSubscribed(self):
3691.163.4 by Brad Bollenbach
checkpoint
694
        """Is the user subscribed to this bug?"""
3691.163.6 by Brad Bollenbach
test and implement a basic web UI for unsubscribing the current user or their teams from dupes of the current bug
695
        return (
696
            self.context.bug.isSubscribed(self.user) or
697
            self.context.bug.isSubscribedToDupes(self.user))
3498.7.1 by Bjorn Tillenius
checkpoint commit in order to merge in rf.
698
3554.1.48 by Brad Bollenbach
Fix bug 49598 (Unable to unsubscribe from private bug)
699
    def render(self):
4974.2.1 by Curtis Hovey
Fixes per pylint.
700
        """Render the bug list if the user has permission to see the bug."""
3554.1.49 by Brad Bollenbach
review fixes
701
        # Prevent normal rendering when redirecting to the bug list
702
        # after unsubscribing from a private bug, because rendering the
703
        # bug page would raise Unauthorized errors!
704
        if self._redirecting_to_bug_list:
3554.1.48 by Brad Bollenbach
Fix bug 49598 (Unable to unsubscribe from private bug)
705
            return u''
706
        else:
707
            return LaunchpadView.render(self)
708
4496.3.4 by Bjorn Tillenius
factor out nomination code.
709
    def _nominateBug(self, series):
710
        """Nominate the bug for the series and redirect to the bug page."""
711
        self.context.bug.addNomination(self.user, series)
712
        self.request.response.addInfoNotification(
5594.1.29 by Maris Fogels
Rework based reviewer feedback.
713
            'This bug has been nominated to be fixed in %s.' %
5594.1.24 by Maris Fogels
Fixed a call to addInfoNotification() that used the old API.
714
                series.bugtargetdisplayname)
4496.3.4 by Bjorn Tillenius
factor out nomination code.
715
        self.request.response.redirect(canonical_url(self.context))
716
3691.62.31 by kiko
Don't display duplicated comments, and don't display the original comment if the description was edited, instead offering a link to it. Resets the formatting of the tags to its original format.
717
    @cachedproperty
718
    def comments(self):
4974.2.1 by Curtis Hovey
Fixes per pylint.
719
        """Return the bugtask's comments."""
12929.7.46 by j.c.sackett
Lint fixes.
720
        show_spam_controls = check_permission(
721
            'launchpad.Admin', self.context.bug)
12376.1.1 by Robert Collins
Refactoring to start getting slice info where we need it.
722
        return get_comments_for_bugtask(self.context, truncate=True,
12929.7.39 by j.c.sackett
Added spam controls to bug comments.
723
            for_display=True, show_spam_controls=show_spam_controls)
3691.62.31 by kiko
Don't display duplicated comments, and don't display the original comment if the description was edited, instead offering a link to it. Resets the formatting of the tags to its original format.
724
8071.6.4 by Graham Binns
Added BugTaskView.activity_by_date.
725
    @cachedproperty
12075.3.3 by Gavin Panella
Factor out the selection of interesting activity.
726
    def interesting_activity(self):
727
        """A sequence of interesting bug activity."""
12075.3.25 by Gavin Panella
Make the implementation of interesting_activity more self-explanatory.
728
        bug_change_re = (
729
            'affects|description|security vulnerability|'
730
            'summary|tags|visibility')
12075.3.3 by Gavin Panella
Factor out the selection of interesting activity.
731
        bugtask_change_re = (
732
            '[a-z0-9][a-z0-9\+\.\-]+( \([A-Za-z0-9\s]+\))?: '
733
            '(assignee|importance|milestone|status)')
12075.3.20 by Gavin Panella
Simplify interesting_activity by folding the predicate into the comprehension.
734
        interesting_match = re.compile(
12075.3.25 by Gavin Panella
Make the implementation of interesting_activity more self-explanatory.
735
            "^(%s|%s)$" % (bug_change_re, bugtask_change_re)).match
12075.3.3 by Gavin Panella
Factor out the selection of interesting activity.
736
        return tuple(
737
            BugActivityItem(activity)
738
            for activity in self.context.bug.activity
12075.3.20 by Gavin Panella
Simplify interesting_activity by folding the predicate into the comprehension.
739
            if interesting_match(activity.whatchanged) is not None)
12075.3.3 by Gavin Panella
Factor out the selection of interesting activity.
740
741
    @cachedproperty
8017.1.2 by Graham Binns
Big Massive Hack.
742
    def activity_and_comments(self):
11010.4.22 by Bryce Harrington
Add pydocs description for activity_and_comments
743
        """Build list of comments interleaved with activities
744
745
        When activities occur on the same day a comment was posted,
746
        encapsulate them with that comment.  For the remainder, group
747
        then as if owned by the person who posted the first action
748
        that day.
749
12075.3.21 by Gavin Panella
Rename newest_comment(s) to recent_comment(s).
750
        If the number of comments exceeds the configured maximum limit, the
751
        list will be truncated to just the first and last sets of comments.
752
753
        The division between the most recent and oldest is marked by an entry
754
        in the list with the key 'num_hidden' defined.
11010.4.22 by Bryce Harrington
Add pydocs description for activity_and_comments
755
        """
11010.4.11 by Bryce Harrington
Assert configuration settings can't lead to weird corner case.
756
        # Ensure truncation results in < max_length comments as expected
757
        assert(config.malone.comments_list_truncate_oldest_to
758
               + config.malone.comments_list_truncate_newest_to
759
               < config.malone.comments_list_max_length)
760
12376.1.2 by Robert Collins
Basic implementation in place, tests not updated.
761
        if not self.visible_comments_truncated_for_display:
13023.3.2 by William Grant
Lint.
762
            comments = self.comments
12376.1.2 by Robert Collins
Basic implementation in place, tests not updated.
763
        else:
12376.1.4 by Robert Collins
Fix known bugs.
764
            # the comment function takes 0-offset counts where comment 0 is
765
            # the initial description, so we need to add one to the limits
766
            # to adjust.
767
            oldest_count = 1 + self.visible_initial_comments
13023.3.2 by William Grant
Lint.
768
            new_count = 1 + self.total_comments - self.visible_recent_comments
12929.7.46 by j.c.sackett
Lint fixes.
769
            show_spam_controls = check_permission(
770
                'launchpad.Admin', self.context.bug)
12376.1.2 by Robert Collins
Basic implementation in place, tests not updated.
771
            comments = get_comments_for_bugtask(
772
                self.context, truncate=True, for_display=True,
773
                slice_info=[
12929.7.39 by j.c.sackett
Added spam controls to bug comments.
774
                    slice(None, oldest_count), slice(new_count, None)],
775
                show_spam_controls=show_spam_controls)
12840.7.2 by j.c.sackett
Test pass, and we didn't have to throw in a ton of db lookups.
776
777
        visible_comments = get_visible_comments(
778
            comments, user=self.user)
11010.4.2 by Bryce Harrington
Display a visual-break element in the UI between new and old comments
779
12075.3.9 by Gavin Panella
Use group_comments_with_activity() in activity_and_comments.
780
        event_groups = group_comments_with_activity(
12376.1.2 by Robert Collins
Basic implementation in place, tests not updated.
781
            comments=visible_comments,
12075.3.9 by Gavin Panella
Use group_comments_with_activity() in activity_and_comments.
782
            activities=self.interesting_activity)
783
784
        def group_activities_by_target(activities):
785
            activities = sorted(
12075.3.18 by Gavin Panella
Make sure grouped activities are still in chronological order.
786
                activities, key=attrgetter(
12075.3.19 by Gavin Panella
Order by activity date before grouping by target.
787
                    "datechanged", "target", "attribute"))
12075.3.9 by Gavin Panella
Use group_comments_with_activity() in activity_and_comments.
788
            return [
789
                {"target": target, "activity": list(activity)}
790
                for target, activity in groupby(
791
                    activities, attrgetter("target"))]
792
793
        def comment_event_dict(comment):
794
            actors = set(activity.person for activity in comment.activity)
795
            actors.add(comment.owner)
796
            assert len(actors) == 1, actors
797
            dates = set(activity.datechanged for activity in comment.activity)
798
            dates.add(comment.datecreated)
799
            comment.activity = group_activities_by_target(comment.activity)
800
            return {
801
                "comment": comment,
802
                "date": min(dates),
803
                "person": actors.pop(),
804
                }
805
806
        def activity_event_dict(activities):
807
            actors = set(activity.person for activity in activities)
808
            assert len(actors) == 1, actors
809
            dates = set(activity.datechanged for activity in activities)
810
            return {
811
                "activity": group_activities_by_target(activities),
812
                "date": min(dates),
813
                "person": actors.pop(),
814
                }
815
816
        def event_dict(event_group):
817
            if isinstance(event_group, list):
818
                return activity_event_dict(event_group)
819
            else:
820
                return comment_event_dict(event_group)
821
12075.3.11 by Gavin Panella
Insert the separator into the event list.
822
        events = map(event_dict, event_groups)
823
12376.1.2 by Robert Collins
Basic implementation in place, tests not updated.
824
        # Insert blanks if we're showing only a subset of the comment list.
825
        if self.visible_comments_truncated_for_display:
12075.3.21 by Gavin Panella
Rename newest_comment(s) to recent_comment(s).
826
            # Find the oldest recent comment in the event list.
12376.1.2 by Robert Collins
Basic implementation in place, tests not updated.
827
            index = 0
828
            prev_comment = None
829
            while index < len(events):
830
                event = events[index]
831
                comment = event.get("comment")
832
                if prev_comment is None:
833
                    prev_comment = comment
834
                    index += 1
835
                    continue
836
                if comment is None:
837
                    index += 1
838
                    continue
839
                if prev_comment.index + 1 != comment.index:
840
                    # There is a gap here, record it.
841
                    separator = {
842
                        'date': prev_comment.datecreated,
12622.5.1 by Curtis Hovey
Always remove the bugtask milestone when retargeting the product.
843
                        'num_hidden': comment.index - prev_comment.index,
12376.1.2 by Robert Collins
Basic implementation in place, tests not updated.
844
                        }
845
                    events.insert(index, separator)
846
                    index += 1
847
                prev_comment = comment
848
                index += 1
849
        return events
8017.1.2 by Graham Binns
Big Massive Hack.
850
12376.1.2 by Robert Collins
Basic implementation in place, tests not updated.
851
    @property
852
    def visible_initial_comments(self):
853
        """How many initial comments are being shown."""
854
        return config.malone.comments_list_truncate_oldest_to
855
856
    @property
857
    def visible_recent_comments(self):
858
        """How many recent comments are being shown."""
859
        return config.malone.comments_list_truncate_newest_to
860
861
    @cachedproperty
7709.2.12 by Gavin Panella
Change getBugCommentsForDisplay() into several properties.
862
    def visible_comments_truncated_for_display(self):
11010.4.9 by Bryce Harrington
Also verb needed!
863
        """Whether the visible comment list is truncated for display."""
12376.1.2 by Robert Collins
Basic implementation in place, tests not updated.
864
        show_all = (self.request.form_ng.getOne('comments') == 'all')
865
        if show_all:
866
            return False
867
        max_comments = config.malone.comments_list_max_length
868
        return self.total_comments > max_comments
869
870
    @cachedproperty
871
    def total_comments(self):
872
        """We count all comments because the db cannot do visibility yet."""
12376.1.4 by Robert Collins
Fix known bugs.
873
        return self.context.bug.bug_messages.count() - 1
3691.62.31 by kiko
Don't display duplicated comments, and don't display the original comment if the description was edited, instead offering a link to it. Resets the formatting of the tags to its original format.
874
875
    def wasDescriptionModified(self):
876
        """Return a boolean indicating whether the description was modified"""
12403.1.1 by Robert Collins
Expose Bug._indexed_messages and use it to generate a much cheaper query for determining if the bugs description was changed.
877
        return (self.context.bug._indexed_messages(
878
            include_content=True, include_parents=False)[0].text_contents !=
12376.1.2 by Robert Collins
Basic implementation in place, tests not updated.
879
            self.context.bug.description)
3607.4.4 by Bjorn Tillenius
add links to the single-comment view from the main bug page.
880
4190.5.15 by Tim Penhey
Updates following review comments
881
    @cachedproperty
8698.10.4 by Paul Hummer
Fixed references to broken code
882
    def linked_branches(self):
4190.5.15 by Tim Penhey
Updates following review comments
883
        """Filter out the bug_branch links to non-visible private branches."""
8698.10.4 by Paul Hummer
Fixed references to broken code
884
        linked_branches = []
885
        for linked_branch in self.context.bug.linked_branches:
886
            if check_permission('launchpad.View', linked_branch.branch):
8698.10.5 by Paul Hummer
Fixed more tests
887
                linked_branches.append(linked_branch)
8698.10.4 by Paul Hummer
Fixed references to broken code
888
        return linked_branches
4190.5.15 by Tim Penhey
Updates following review comments
889
5020.3.6 by Curtis Hovey
Added pagetest and UI for expiration notices. We *really* need to replace the
890
    @property
891
    def days_to_expiration(self):
892
        """Return the number of days before the bug is expired, or None."""
11057.8.2 by Brian Murray
modify can_expire to use the days_before_expiration config option
893
        if not self.context.bug.isExpirable(days_old=0):
5020.3.6 by Curtis Hovey
Added pagetest and UI for expiration notices. We *really* need to replace the
894
            return None
895
5020.3.9 by Curtis Hovey
Revisions per review.
896
        expire_after = timedelta(days=config.malone.days_before_expiration)
897
        expiration_date = self.context.bug.date_last_updated + expire_after
12075.3.7 by Gavin Panella
Import utc from pytz, not UTC.
898
        remaining_time = expiration_date - datetime.now(utc)
5020.3.9 by Curtis Hovey
Revisions per review.
899
        return remaining_time.days
5020.3.6 by Curtis Hovey
Added pagetest and UI for expiration notices. We *really* need to replace the
900
5355.1.1 by Graham Binns
Bug expirations are now talked about in the sense of marking a bug for
901
    @property
902
    def expiration_message(self):
903
        """Return a message indicating the time to expiration for the bug.
904
905
        If the expiration date of the bug has already passed, the
906
        message returned will indicate this. This deals with situations
907
        where a bug is due to be marked invalid but has not yet been
908
        dealt with by the bug expiration script.
909
910
        If the bug is not due to be expired None will be returned.
911
        """
11057.8.2 by Brian Murray
modify can_expire to use the days_before_expiration config option
912
        if not self.context.bug.isExpirable(days_old=0):
5355.1.1 by Graham Binns
Bug expirations are now talked about in the sense of marking a bug for
913
            return None
914
915
        days_to_expiration = self.days_to_expiration
5355.1.2 by Graham Binns
Reworded messages slightly and tweaked the was expired/will be expired threshold.
916
        if days_to_expiration <= 0:
5355.1.1 by Graham Binns
Bug expirations are now talked about in the sense of marking a bug for
917
            # We should always display a positive number to the user,
918
            # whether we're talking about the past or the future.
919
            days_to_expiration = -days_to_expiration
5355.1.2 by Graham Binns
Reworded messages slightly and tweaked the was expired/will be expired threshold.
920
            message = ("This bug report was marked for expiration %i days "
921
                "ago.")
5355.1.1 by Graham Binns
Bug expirations are now talked about in the sense of marking a bug for
922
        else:
923
            message = ("This bug report will be marked for expiration in %i "
924
                "days if no further activity occurs.")
925
926
        return message % days_to_expiration
927
8029.1.1 by Tom Berger
display official tags using a different style, and group them before the unofficial ones
928
    @property
929
    def official_tags(self):
930
        """The list of official tags for this bug."""
11582.2.2 by Robert Collins
Probably broken, but takes 46 queries off of a baseline BugTask:+index.
931
        target_official_tags = set(self.context.bug.official_tags)
8029.1.1 by Tom Berger
display official tags using a different style, and group them before the unofficial ones
932
        return [tag for tag in self.context.bug.tags
933
                if tag in target_official_tags]
934
935
    @property
936
    def unofficial_tags(self):
937
        """The list of unofficial tags for this bug."""
11582.2.2 by Robert Collins
Probably broken, but takes 46 queries off of a baseline BugTask:+index.
938
        target_official_tags = set(self.context.bug.official_tags)
8029.1.1 by Tom Berger
display official tags using a different style, and group them before the unofficial ones
939
        return [tag for tag in self.context.bug.tags
940
                if tag not in target_official_tags]
941
8029.2.2 by Tom Berger
merge changes from rocketfuel and resolve conflicts
942
    @property
8029.2.1 by Tom Berger
Insert a JSON representation of the available official tags into the bug page
943
    def available_official_tags_js(self):
944
        """Return the list of available official tags for the bug as JSON.
945
946
        The list comprises of the official tags for all targets for which the
7675.759.10 by Brad Crittenden
Fixed lint issues
947
        bug has a task. It is returned as Javascript snippet, to be embedded
948
        in the bug page.
8029.2.1 by Tom Berger
Insert a JSON representation of the available official tags into the bug page
949
        """
11582.2.3 by Robert Collins
Review feedback.
950
        # Unwrap the security proxy. - official_tags is a security proxy
951
        # wrapped list.
11582.2.2 by Robert Collins
Probably broken, but takes 46 queries off of a baseline BugTask:+index.
952
        available_tags = list(self.context.bug.official_tags)
953
        return 'var available_official_tags = %s;' % dumps(available_tags)
8029.2.1 by Tom Berger
Insert a JSON representation of the available official tags into the bug page
954
8137.17.24 by Barry Warsaw
thread merge
955
    @property
956
    def user_is_admin(self):
957
        """Is the user a Launchpad admin?"""
958
        return check_permission('launchpad.Admin', self.context)
959
8971.27.1 by Deryck Hodge
A straight diff & patch of mars' bd-editor branch, to
960
    @property
8971.27.21 by Deryck Hodge
Rename bug_description to bug_description_html.
961
    def bug_description_html(self):
8971.27.1 by Deryck Hodge
A straight diff & patch of mars' bd-editor branch, to
962
        """The bug's description as HTML."""
12268.3.19 by Tim Penhey
Reorder the title parameter to match the call sites.
963
        bug = self.context.bug
964
        description = IBug['description']
965
        title = "Bug Description"
966
        edit_url = canonical_url(self.context, view_name='+edit')
8971.27.18 by Deryck Hodge
Rename the widget, to not be bugs description specific.
967
        return TextAreaEditorWidget(
12268.3.19 by Tim Penhey
Reorder the title parameter to match the call sites.
968
            bug, description, title, edit_url=edit_url)
8971.27.1 by Deryck Hodge
A straight diff & patch of mars' bd-editor branch, to
969
7675.470.5 by Tom Berger
interim commit
970
    @property
971
    def bug_heat_html(self):
972
        """HTML representation of the bug heat."""
10481.1.4 by Deryck Hodge
Make packages use the distro context on bug pages.
973
        if IDistributionSourcePackage.providedBy(self.context.target):
974
            return bugtask_heat_html(
975
                self.context, target=self.context.distribution)
976
        else:
977
            return bugtask_heat_html(self.context)
7675.470.5 by Tom Berger
interim commit
978
12183.10.7 by Huw Wilkins
Moved privacy notification javascript to appropriate file. Fixed the notification so it now appears or hides when the bug is made private/public.
979
    @property
980
    def privacy_notice_classes(self):
981
        if not self.context.bug.private:
982
            return 'hidden'
983
        else:
984
            return ''
985
5355.1.1 by Graham Binns
Bug expirations are now talked about in the sense of marking a bug for
986
7675.553.29 by Deryck Hodge
Global replace max_heat to max_bug_heat.
987
def calculate_heat_display(heat, max_bug_heat):
7675.554.4 by Tom Berger
Scale the value used for calculating the number of heat flames to display so that it produces a reasonable amount of flames for the hottest bugs.
988
    """Calculate the number of heat 'flames' to display."""
989
    heat = float(heat)
7675.553.29 by Deryck Hodge
Global replace max_heat to max_bug_heat.
990
    max_bug_heat = float(max_bug_heat)
7675.565.5 by Tom Berger
avoid division by zero
991
    if max_bug_heat == 0:
992
        return 0
7675.553.29 by Deryck Hodge
Global replace max_heat to max_bug_heat.
993
    if heat / max_bug_heat < 0.33333:
7675.554.4 by Tom Berger
Scale the value used for calculating the number of heat flames to display so that it produces a reasonable amount of flames for the hottest bugs.
994
        return 0
7675.562.1 by Abel Deuring
fix for bug 528374: Flames misplaced on bug report page; also, guard better against pathological cases of max_heat values
995
    if heat / max_bug_heat < 0.66666 or max_bug_heat < 2:
7675.553.29 by Deryck Hodge
Global replace max_heat to max_bug_heat.
996
        return int(floor((heat / max_bug_heat) * 4))
7675.554.4 by Tom Berger
Scale the value used for calculating the number of heat flames to display so that it produces a reasonable amount of flames for the hottest bugs.
997
    else:
7675.562.1 by Abel Deuring
fix for bug 528374: Flames misplaced on bug report page; also, guard better against pathological cases of max_heat values
998
        heat_index = int(floor((log(heat) / log(max_bug_heat)) * 4))
999
        # ensure that we never return a value > 4, even if
1000
        # max_bug_heat is outdated.
1001
        return min(heat_index, 4)
7675.554.4 by Tom Berger
Scale the value used for calculating the number of heat flames to display so that it produces a reasonable amount of flames for the hottest bugs.
1002
1003
7675.568.3 by Tom Berger
When creating the bug heat flames display, compare to the max heat of the current context, not the target of the bugtask.
1004
def bugtask_heat_html(bugtask, target=None):
7675.554.1 by Tom Berger
use the bugtask target max_heat to render bug heat
1005
    """Render the HTML representing bug heat for a given bugask."""
7675.568.3 by Tom Berger
When creating the bug heat flames display, compare to the max heat of the current context, not the target of the bugtask.
1006
    if target is None:
1007
        target = bugtask.target
1008
    max_bug_heat = target.max_bug_heat
7675.553.30 by Deryck Hodge
Account for max_bug_heat being NULL by default.
1009
    if max_bug_heat is None:
7675.553.29 by Deryck Hodge
Global replace max_heat to max_bug_heat.
1010
        max_bug_heat = 5000
1011
    heat_ratio = calculate_heat_display(bugtask.bug.heat, max_bug_heat)
7675.555.1 by Abel Deuring
devel merge conflict resolved
1012
    html = (
10746.1.10 by matthew.revell at canonical
Fixes the failure of bug-heat-view test.
1013
        '<span><a href="/+help/bug-heat.html" target="help" class="icon"><img'
1014
        ' src="/@@/bug-heat-%(ratio)i.png" '
1015
        'alt="%(ratio)i out of 4 heat flames" title="Heat: %(heat)i" /></a>'
7675.562.1 by Abel Deuring
fix for bug 528374: Flames misplaced on bug report page; also, guard better against pathological cases of max_heat values
1016
        '</span>'
7675.555.1 by Abel Deuring
devel merge conflict resolved
1017
        % {'ratio': heat_ratio, 'heat': bugtask.bug.heat})
7675.554.1 by Tom Berger
use the bugtask target max_heat to render bug heat
1018
    return html
1019
1020
3283.3.1 by Brad Bollenbach
create a new branch for bzr integration, to avoid 3 hour merge time
1021
class BugTaskPortletView:
4974.2.1 by Curtis Hovey
Fixes per pylint.
1022
    """A portlet for displaying a bug's bugtasks."""
1023
2487 by Canonical.com Patch Queue Manager
[r=BjornT] Malone URL changes. Yep. Also: contextualize the bug page,
1024
    def alsoReportedIn(self):
1025
        """Return a list of IUpstreamBugTasks in which this bug is reported.
1026
1027
        If self.context is an IUpstreamBugTasks, it will be excluded
1028
        from this list.
1029
        """
1030
        return [
1031
            task for task in self.context.bug.bugtasks
1032
            if task.id is not self.context.id]
1033
1034
8935.1.1 by Tom Berger
Set the controls of the inline bugtask form when setting status and importance. Also produce the initialization values for the bugtask row in a saner way, using simplejson from the view code.
1035
def get_prefix(bugtask):
1036
    """Return a prefix that can be used for this form.
1037
1038
    The prefix is constructed using the name of the bugtask's target so as
1039
    to ensure that it's unique within the context of a bug. This is needed
1040
    in order to included multiple edit forms on the bug page, while still
1041
    keeping the field ids unique.
1042
    """
1043
    parts = []
13479.2.7 by William Grant
Rip them out of browser.bugtask.
1044
    parts.append(bugtask.pillar.name)
1045
1046
    series = bugtask.productseries or bugtask.distroseries
1047
    if series:
1048
        parts.append(series.name)
1049
1050
    if bugtask.sourcepackagename is not None:
1051
        parts.append(bugtask.sourcepackagename.name)
1052
8935.1.1 by Tom Berger
Set the controls of the inline bugtask form when setting status and importance. Also produce the initialization values for the bugtask row in a saner way, using simplejson from the view code.
1053
    return '_'.join(parts)
1054
1055
10788.5.2 by Abel Deuring
ordinary users can (un)assign a bug task only to hemselves and their teams
1056
def get_assignee_vocabulary(context):
1057
    """The vocabulary of bug task assignees the current user can set."""
1058
    if context.userCanSetAnyAssignee(getUtility(ILaunchBag).user):
1059
        return 'ValidAssignee'
1060
    else:
1061
        return 'AllUserTeamsParticipation'
1062
1063
10888.3.1 by Graham Binns
Moved BugWatch.getLastErrorMessage() into view code, where it actually belongs.
1064
class BugTaskBugWatchMixin:
10901.1.5 by Graham Binns
Added links to the new help to bugtask-tasks-and-blahblahblah.pt and bugtask-edit-form.pt.
1065
    """A mixin to be used where a BugTask view displays BugWatch data."""
10888.3.1 by Graham Binns
Moved BugWatch.getLastErrorMessage() into view code, where it actually belongs.
1066
1067
    @property
1068
    def bug_watch_error_message(self):
1069
        """Return a browser-useable error message for a bug watch."""
1070
        if not self.context.bugwatch:
1071
            return None
1072
1073
        bug_watch = self.context.bugwatch
1074
        if not bug_watch.last_error_type:
1075
            return None
1076
1077
        error_message_mapping = {
1078
            BugWatchActivityStatus.BUG_NOT_FOUND: "%(bugtracker)s bug #"
1079
                "%(bug)s appears not to exist. Check that the bug "
1080
                "number is correct.",
1081
            BugWatchActivityStatus.CONNECTION_ERROR: "Launchpad couldn't "
1082
                "connect to %(bugtracker)s.",
1083
            BugWatchActivityStatus.INVALID_BUG_ID: "Bug ID %(bug)s isn't "
1084
                "valid on %(bugtracker)s. Check that the bug ID is "
1085
                "correct.",
1086
            BugWatchActivityStatus.TIMEOUT: "Launchpad's connection to "
1087
                "%(bugtracker)s timed out.",
1088
            BugWatchActivityStatus.UNKNOWN: "Launchpad couldn't import bug "
1089
                "#%(bug)s from " "%(bugtracker)s.",
1090
            BugWatchActivityStatus.UNPARSABLE_BUG: "Launchpad couldn't "
1091
                "extract a status from %(bug)s on %(bugtracker)s.",
1092
            BugWatchActivityStatus.UNPARSABLE_BUG_TRACKER: "Launchpad "
1093
                "couldn't determine the version of %(bugtrackertype)s "
1094
                "running on %(bugtracker)s.",
1095
            BugWatchActivityStatus.UNSUPPORTED_BUG_TRACKER: "Launchpad "
1096
                "doesn't support importing bugs from %(bugtrackertype)s"
1097
                " bug trackers.",
1098
            BugWatchActivityStatus.PRIVATE_REMOTE_BUG: "The bug is marked as "
1099
                "private on the remote bug tracker. Launchpad cannot import "
1100
                "the status of private remote bugs.",
1101
            }
1102
1103
        if bug_watch.last_error_type in error_message_mapping:
1104
            message = error_message_mapping[bug_watch.last_error_type]
10901.1.9 by Graham Binns
Removed an unused elif.
1105
        else:
10888.3.1 by Graham Binns
Moved BugWatch.getLastErrorMessage() into view code, where it actually belongs.
1106
            message = bug_watch.last_error_type.description
1107
1108
        error_data = {
1109
            'bug': bug_watch.remotebug,
1110
            'bugtracker': bug_watch.bugtracker.title,
1111
            'bugtrackertype': bug_watch.bugtracker.bugtrackertype.title}
1112
10888.3.2 by Graham Binns
Added a link to the help page for bug watch errors.
1113
        return {
1114
            'message': message % error_data,
10901.1.5 by Graham Binns
Added links to the new help to bugtask-tasks-and-blahblahblah.pt and bugtask-edit-form.pt.
1115
            'help_url': '%s#%s' % (
1116
                canonical_url(bug_watch, view_name="+error-help"),
10888.3.2 by Graham Binns
Added a link to the help page for bug watch errors.
1117
                bug_watch.last_error_type.name),
1118
            }
10888.3.1 by Graham Binns
Moved BugWatch.getLastErrorMessage() into view code, where it actually belongs.
1119
1120
1121
class BugTaskEditView(LaunchpadEditFormView, BugTaskBugWatchMixin):
2938.2.3 by Brad Bollenbach
add form error handling for the Comment on Change
1122
    """The view class used for the task +editstatus page."""
3063.2.25 by Bjorn Tillenius
make bugtasks linked to bug watches read-only except for the bugwatch. (web UI only)
1123
4611.6.26 by Graham Binns
Fixed some broken pagetests; removed unnecessary schema property from BugTaskEditView
1124
    schema = IBugTask
6291.1.6 by Bjorn Tillenius
cache the milestone vocabularies.
1125
    milestone_source = None
6291.1.9 by Bjorn Tillenius
fix test failures.
1126
    user_is_subscribed = None
6291.1.8 by Bjorn Tillenius
cache bug.isSubscribed(useelf.ser)
1127
    edit_form = ViewPageTemplateFile('../templates/bugtask-edit-form.pt')
4611.6.42 by Graham Binns
Comment alterations.
1128
1129
    # The field names that we use by default. This list will be mutated
1130
    # depending on the current context and the permissions of the user viewing
1131
    # the form.
4611.6.47 by Graham Binns
Some refactoring of BugTaskEditView of BugTaskEditView per thumper's comments.
1132
    default_field_names = ['assignee', 'bugwatch', 'importance', 'milestone',
1133
                           'product', 'sourcepackagename', 'status',
1134
                           'statusexplanation']
4611.6.46 by Graham Binns
Fixed pyflakes issues. Code re-arrangement in BugTaskEditView declaration.
1135
    custom_widget('sourcepackagename', BugTaskSourcePackageNameWidget)
1136
    custom_widget('bugwatch', BugTaskBugWatchWidget)
1137
    custom_widget('assignee', BugTaskAssigneeWidget)
4611.6.3 by Graham Binns
Refactored BugTaskEditView.field_names
1138
6291.1.9 by Bjorn Tillenius
fix test failures.
1139
    def initialize(self):
1140
        # Initialize user_is_subscribed, if it hasn't already been set.
1141
        if self.user_is_subscribed is None:
1142
            self.user_is_subscribed = self.context.bug.isSubscribed(self.user)
12561.3.12 by Curtis Hovey
Fixed broken view while rewriting a broken test.
1143
        super(BugTaskEditView, self).initialize()
6291.1.9 by Bjorn Tillenius
fix test failures.
1144
11118.3.1 by Curtis Hovey
Removed BugTaskSOP, inlined page_titles, updated page_titles for specialised bugtask
1145
    page_title = 'Edit status'
6291.1.9 by Bjorn Tillenius
fix test failures.
1146
4611.6.53 by Graham Binns
Made field_names a cachedproperty.
1147
    @cachedproperty
4611.6.47 by Graham Binns
Some refactoring of BugTaskEditView of BugTaskEditView per thumper's comments.
1148
    def field_names(self):
4974.2.1 by Curtis Hovey
Fixes per pylint.
1149
        """Return the field names that can be edited by the user."""
6164.1.11 by Gavin Panella
Post-review fixes.
1150
        field_names = set(self.default_field_names)
4611.6.47 by Graham Binns
Some refactoring of BugTaskEditView of BugTaskEditView per thumper's comments.
1151
1152
        # The fields that we present to the users change based upon the
1153
        # current context and the user's permissions, so we update field_names
1154
        # with any fields that may need to be added.
6164.1.11 by Gavin Panella
Post-review fixes.
1155
        field_names.update(self.editable_field_names)
4611.6.47 by Graham Binns
Some refactoring of BugTaskEditView of BugTaskEditView per thumper's comments.
1156
6164.1.11 by Gavin Panella
Post-review fixes.
1157
        # To help with caching, return an immutable object.
1158
        return frozenset(field_names)
4611.6.47 by Graham Binns
Some refactoring of BugTaskEditView of BugTaskEditView per thumper's comments.
1159
4611.6.48 by Graham Binns
Refactored BugTaskEditView._getEditableFeFieldNames() as a cachedproperty.
1160
    @cachedproperty
1161
    def editable_field_names(self):
1162
        """Return the names of fields the user has permission to edit."""
1163
        if self.context.target_uses_malone:
1164
            # Don't edit self.field_names directly, because it's shared by all
1165
            # BugTaskEditView instances.
6164.1.3 by Gavin Panella
Show the Importance and Status widgets.
1166
            editable_field_names = set(self.default_field_names)
1167
            editable_field_names.discard('bugwatch')
4611.6.48 by Graham Binns
Refactored BugTaskEditView._getEditableFeFieldNames() as a cachedproperty.
1168
6916.1.3 by Curtis Hovey
Removed bad commas in XXX comments.
1169
            # XXX: Brad Bollenbach 2006-09-29 bug=63000: Permission checking
1170
            # doesn't belong here!
6164.1.3 by Gavin Panella
Show the Importance and Status widgets.
1171
            if ('milestone' in editable_field_names and
1172
                not self.userCanEditMilestone()):
4611.6.48 by Graham Binns
Refactored BugTaskEditView._getEditableFeFieldNames() as a cachedproperty.
1173
                editable_field_names.remove("milestone")
1174
6164.1.3 by Gavin Panella
Show the Importance and Status widgets.
1175
            if ('importance' in editable_field_names and
1176
                not self.userCanEditImportance()):
4611.6.48 by Graham Binns
Refactored BugTaskEditView._getEditableFeFieldNames() as a cachedproperty.
1177
                editable_field_names.remove("importance")
1178
        else:
11626.3.10 by Curtis Hovey
Hush lints epic complaints about the changes files.
1179
            editable_field_names = set(('bugwatch', ))
13479.2.7 by William Grant
Rip them out of browser.bugtask.
1180
            if not IProduct.providedBy(self.context.target):
4611.6.48 by Graham Binns
Refactored BugTaskEditView._getEditableFeFieldNames() as a cachedproperty.
1181
                #XXX: Bjorn Tillenius 2006-03-01:
1182
                #     Should be possible to edit the product as well,
1183
                #     but that's harder due to complications with bug
1184
                #     watches. The new product might use Launchpad
1185
                #     officially, thus we need to handle that case.
1186
                #     Let's deal with that later.
6164.1.3 by Gavin Panella
Show the Importance and Status widgets.
1187
                editable_field_names.add('sourcepackagename')
4611.6.48 by Graham Binns
Refactored BugTaskEditView._getEditableFeFieldNames() as a cachedproperty.
1188
            if self.context.bugwatch is None:
6164.1.3 by Gavin Panella
Show the Importance and Status widgets.
1189
                editable_field_names.update(('status', 'assignee'))
4611.6.52 by Graham Binns
Removed recursion.
1190
                if ('importance' in self.default_field_names
1191
                    and self.userCanEditImportance()):
6164.1.3 by Gavin Panella
Show the Importance and Status widgets.
1192
                    editable_field_names.add('importance')
1193
            else:
1194
                bugtracker = self.context.bugwatch.bugtracker
1195
                if bugtracker.bugtrackertype == BugTrackerType.EMAILADDRESS:
1196
                    editable_field_names.add('status')
1197
                    if ('importance' in self.default_field_names
1198
                        and self.userCanEditImportance()):
1199
                        editable_field_names.add('importance')
4611.6.48 by Graham Binns
Refactored BugTaskEditView._getEditableFeFieldNames() as a cachedproperty.
1200
6164.1.11 by Gavin Panella
Post-review fixes.
1201
        # To help with caching, return an immutable object.
6164.1.3 by Gavin Panella
Show the Importance and Status widgets.
1202
        return frozenset(editable_field_names)
4611.6.48 by Graham Binns
Refactored BugTaskEditView._getEditableFeFieldNames() as a cachedproperty.
1203
4611.6.53 by Graham Binns
Made field_names a cachedproperty.
1204
    @property
5398.2.1 by Curtis Hovey
Fixed +editstatus not permit users to edit bugs that were converted into questions.
1205
    def is_question(self):
1206
        """Return True or False if this bug was converted into a question.
1207
1208
        Bugtasks cannot be edited if the bug was converted into a question.
1209
        """
1210
        return self.context.bug.getQuestionCreatedFromBug() is not None
1211
1212
    @property
4611.6.53 by Graham Binns
Made field_names a cachedproperty.
1213
    def next_url(self):
1214
        """See `LaunchpadFormView`."""
1215
        return canonical_url(self.context)
1216
1217
    @property
4611.6.58 by Graham Binns
Replaced initial_values(), which was removed before but was actually needed.
1218
    def initial_values(self):
1219
        """See `LaunchpadFormView.`"""
1220
        field_values = {}
1221
        for name in self.field_names:
1222
            field_values[name] = getattr(self.context, name)
1223
1224
        return field_values
1225
1226
    @property
4611.6.53 by Graham Binns
Made field_names a cachedproperty.
1227
    def prefix(self):
1228
        """Return a prefix that can be used for this form.
1229
1230
        The prefix is constructed using the name of the bugtask's target so as
1231
        to ensure that it's unique within the context of a bug. This is needed
1232
        in order to included multiple edit forms on the bug page, while still
1233
        keeping the field ids unique.
1234
        """
8935.1.1 by Tom Berger
Set the controls of the inline bugtask form when setting status and importance. Also produce the initialization values for the bugtask row in a saner way, using simplejson from the view code.
1235
        return get_prefix(self.context)
4611.6.53 by Graham Binns
Made field_names a cachedproperty.
1236
4611.6.11 by Graham Binns
Fixed the vocabulary for status dropdowns.
1237
    def setUpFields(self):
4611.6.36 by Graham Binns
Whitespace changes and tidying up.
1238
        """Sets up the fields for the bug task edit form.
1239
4611.6.47 by Graham Binns
Some refactoring of BugTaskEditView of BugTaskEditView per thumper's comments.
1240
        See `LaunchpadFormView`.
4611.6.36 by Graham Binns
Whitespace changes and tidying up.
1241
        """
4611.6.47 by Graham Binns
Some refactoring of BugTaskEditView of BugTaskEditView per thumper's comments.
1242
        super(BugTaskEditView, self).setUpFields()
1243
        read_only_field_names = self._getReadOnlyFieldNames()
4611.6.12 by Graham Binns
Refactored widget creation (finally)
1244
4611.6.11 by Graham Binns
Fixed the vocabulary for status dropdowns.
1245
        # The status field is a special case because we alter the vocabulary
1246
        # it uses based on the permissions of the user viewing form.
4611.6.48 by Graham Binns
Refactored BugTaskEditView._getEditableFeFieldNames() as a cachedproperty.
1247
        if 'status' in self.editable_field_names:
4318.3.16 by Gavin Panella
Refactor status transition checks, also allowing project registrant to make some restricted status changes.
1248
            if self.user is None:
10680.2.7 by Abel Deuring
fixed test failures
1249
                status_noshow = set(BugTaskStatus.items)
4318.3.16 by Gavin Panella
Refactor status transition checks, also allowing project registrant to make some restricted status changes.
1250
            else:
10680.2.7 by Abel Deuring
fixed test failures
1251
                status_noshow = set((
1252
                    BugTaskStatus.UNKNOWN, BugTaskStatus.EXPIRED))
1253
                status_noshow.update(
4318.3.16 by Gavin Panella
Refactor status transition checks, also allowing project registrant to make some restricted status changes.
1254
                    status for status in BugTaskStatus.items
1255
                    if not self.context.canTransitionToStatus(
1256
                        status, self.user))
4611.6.11 by Graham Binns
Fixed the vocabulary for status dropdowns.
1257
4318.3.8 by Gavin Panella
Add the Won't Fix bug status.
1258
            if self.context.status in status_noshow:
1259
                # The user has to be able to see the current value.
1260
                status_noshow.remove(self.context.status)
4611.6.11 by Graham Binns
Fixed the vocabulary for status dropdowns.
1261
9055.5.1 by Guilherme Salgado
Remove IDBSchema and some unused code related to it
1262
            # We shouldn't have to build our vocabulary out of (item.title,
1263
            # item) tuples -- iterating over an EnumeratedType gives us
1264
            # ITokenizedTerms that we could use. However, the terms generated
9055.5.2 by Guilherme Salgado
Remove XXXs
1265
            # by EnumeratedType have their name as the token and here we need
1266
            # the title as the token for backwards compatibility.
9055.5.1 by Guilherme Salgado
Remove IDBSchema and some unused code related to it
1267
            status_items = [
1268
                (item.title, item) for item in BugTaskStatus.items
1269
                if item not in status_noshow]
4318.3.8 by Gavin Panella
Add the Won't Fix bug status.
1270
            status_field = Choice(
9055.5.1 by Guilherme Salgado
Remove IDBSchema and some unused code related to it
1271
                __name__='status', title=self.schema['status'].title,
1272
                vocabulary=SimpleVocabulary.fromItems(status_items))
4611.6.11 by Graham Binns
Fixed the vocabulary for status dropdowns.
1273
4611.6.12 by Graham Binns
Refactored widget creation (finally)
1274
            self.form_fields = self.form_fields.omit('status')
6138.3.3 by Bjorn Tillenius
fix lint warnings. add comment.
1275
            self.form_fields += formlib.form.Fields(status_field)
4611.6.12 by Graham Binns
Refactored widget creation (finally)
1276
6291.1.6 by Bjorn Tillenius
cache the milestone vocabularies.
1277
        # If we have a milestone vocabulary already, create a new field
1278
        # to use it, instead of creating a new one.
1279
        if self.milestone_source is not None:
1280
            milestone_source = self.milestone_source
1281
            milestone_field = Choice(
1282
                __name__='milestone',
1283
                title=self.schema['milestone'].title,
1284
                source=milestone_source, required=False)
9119.3.6 by William Grant
Force the milestone widget to be writable in BugTaskEditView.
1285
        else:
9087.5.10 by Tom Berger
some changes post review
1286
            milestone_field = copy_field(
1287
                IBugTask['milestone'], readonly=False)
9119.3.6 by William Grant
Force the milestone widget to be writable in BugTaskEditView.
1288
1289
        self.form_fields = self.form_fields.omit('milestone')
1290
        self.form_fields += formlib.form.Fields(milestone_field)
6291.1.6 by Bjorn Tillenius
cache the milestone vocabularies.
1291
4611.6.47 by Graham Binns
Some refactoring of BugTaskEditView of BugTaskEditView per thumper's comments.
1292
        for field in read_only_field_names:
4611.6.12 by Graham Binns
Refactored widget creation (finally)
1293
            self.form_fields[field].for_display = True
4318.3.9 by Gavin Panella
Pretty-up the bugtask widget code.
1294
4611.6.14 by Graham Binns
Fixed display of read-only status and importance fields
1295
        # In cases where the status or importance fields are read only we give
4611.6.22 by Graham Binns
Fixed a typo
1296
        # them a custom widget so that they are rendered correctly.
4611.6.14 by Graham Binns
Fixed display of read-only status and importance fields
1297
        for field in ['status', 'importance']:
4611.6.47 by Graham Binns
Some refactoring of BugTaskEditView of BugTaskEditView per thumper's comments.
1298
            if field in read_only_field_names:
4611.6.14 by Graham Binns
Fixed display of read-only status and importance fields
1299
                self.form_fields[field].custom_widget = CustomWidgetFactory(
1300
                    DBItemDisplayWidget)
1301
5666.1.2 by Tom Berger
no need to use setUpWidgets, the field can be set up in ... setUpFields (cheers salgado and flacoste for the help)
1302
        if 'importance' not in read_only_field_names:
1303
            # Users shouldn't be able to set a bugtask's importance to
11629.1.2 by Bryce Harrington
spelling in comments (no code or user effect)
1304
            # `UNKNOWN`, only bug watches do that.
5666.1.2 by Tom Berger
no need to use setUpWidgets, the field can be set up in ... setUpFields (cheers salgado and flacoste for the help)
1305
            importance_vocab_items = [
1306
                item for item in BugTaskImportance.items.items
1307
                if item != BugTaskImportance.UNKNOWN]
1308
            self.form_fields = self.form_fields.omit('importance')
6138.3.3 by Bjorn Tillenius
fix lint warnings. add comment.
1309
            self.form_fields += formlib.form.Fields(
5666.1.2 by Tom Berger
no need to use setUpWidgets, the field can be set up in ... setUpFields (cheers salgado and flacoste for the help)
1310
                Choice(__name__='importance',
1311
                       title=_('Importance'),
1312
                       values=importance_vocab_items,
1313
                       default=BugTaskImportance.UNDECIDED))
1314
4318.3.9 by Gavin Panella
Pretty-up the bugtask widget code.
1315
        if self.context.target_uses_malone:
4611.6.12 by Graham Binns
Refactored widget creation (finally)
1316
            self.form_fields = self.form_fields.omit('bugwatch')
4611.6.36 by Graham Binns
Whitespace changes and tidying up.
1317
4611.6.16 by Graham Binns
Fixed bugtask page for packages
1318
        elif (self.context.bugwatch is not None and
1319
            self.form_fields.get('assignee', False)):
4611.6.12 by Graham Binns
Refactored widget creation (finally)
1320
            self.form_fields['assignee'].custom_widget = CustomWidgetFactory(
1321
                AssigneeDisplayWidget)
4611.6.10 by Graham Binns
Began the long, fiddly task of widget refactoring
1322
6415.2.26 by Tom Berger
only make the assignee field editable if there is no linked bugwatch
1323
        if (self.context.bugwatch is None and
1324
            self.form_fields.get('assignee', False)):
1325
            # Make the assignee field editable
6415.2.21 by Tom Berger
the assignee field is readonly in the schema, but readwrite in the form
1326
            self.form_fields = self.form_fields.omit('assignee')
7675.759.1 by Brad Crittenden
Destroy PRIVATE_MEMBERSHIP teams and remove them from the face of the Earth.
1327
            self.form_fields += formlib.form.Fields(PersonChoice(
6415.2.21 by Tom Berger
the assignee field is readonly in the schema, but readwrite in the form
1328
                __name__='assignee', title=_('Assigned to'), required=False,
10788.5.2 by Abel Deuring
ordinary users can (un)assign a bug task only to hemselves and their teams
1329
                vocabulary=get_assignee_vocabulary(self.context),
1330
                readonly=False))
6415.2.21 by Tom Berger
the assignee field is readonly in the schema, but readwrite in the form
1331
            self.form_fields['assignee'].custom_widget = CustomWidgetFactory(
1332
                BugTaskAssigneeWidget)
1333
3554.1.39 by Brad Bollenbach
even more refactoring
1334
    def _getReadOnlyFieldNames(self):
1335
        """Return the names of fields that will be rendered read only."""
1336
        if self.context.target_uses_malone:
1337
            read_only_field_names = []
1338
3553.3.48 by Brad Bollenbach
checkpoint
1339
            if not self.userCanEditMilestone():
3554.1.39 by Brad Bollenbach
even more refactoring
1340
                read_only_field_names.append("milestone")
1341
3553.3.48 by Brad Bollenbach
checkpoint
1342
            if not self.userCanEditImportance():
3554.1.41 by Brad Bollenbach
code review touchups
1343
                read_only_field_names.append("importance")
3554.1.39 by Brad Bollenbach
even more refactoring
1344
        else:
4611.6.48 by Graham Binns
Refactored BugTaskEditView._getEditableFeFieldNames() as a cachedproperty.
1345
            editable_field_names = self.editable_field_names
3554.1.39 by Brad Bollenbach
even more refactoring
1346
            read_only_field_names = [
4611.6.3 by Graham Binns
Refactored BugTaskEditView.field_names
1347
                field_name for field_name in self.field_names
3554.1.40 by Brad Bollenbach
and more refactoring still
1348
                if field_name not in editable_field_names]
3063.2.25 by Bjorn Tillenius
make bugtasks linked to bug watches read-only except for the bugwatch. (web UI only)
1349
3554.1.39 by Brad Bollenbach
even more refactoring
1350
        return read_only_field_names
3554.1.37 by Brad Bollenbach
Restrict permissions for changing Importance to either the product/distro bug contact or, if there isn't one, require launchpad.Edit on the product or distro
1351
3553.3.48 by Brad Bollenbach
checkpoint
1352
    def userCanEditMilestone(self):
3554.1.37 by Brad Bollenbach
Restrict permissions for changing Importance to either the product/distro bug contact or, if there isn't one, require launchpad.Edit on the product or distro
1353
        """Can the user edit the Milestone field?
1354
1355
        If yes, return True, otherwise return False.
1356
        """
6584.1.1 by Tom Berger
move the security predicates to the content objects
1357
        return self.context.userCanEditMilestone(self.user)
3554.1.37 by Brad Bollenbach
Restrict permissions for changing Importance to either the product/distro bug contact or, if there isn't one, require launchpad.Edit on the product or distro
1358
3553.3.48 by Brad Bollenbach
checkpoint
1359
    def userCanEditImportance(self):
3554.1.37 by Brad Bollenbach
Restrict permissions for changing Importance to either the product/distro bug contact or, if there isn't one, require launchpad.Edit on the product or distro
1360
        """Can the user edit the Importance field?
1361
1362
        If yes, return True, otherwise return False.
1363
        """
6584.1.1 by Tom Berger
move the security predicates to the content objects
1364
        return self.context.userCanEditImportance(self.user)
3554.1.37 by Brad Bollenbach
Restrict permissions for changing Importance to either the product/distro bug contact or, if there isn't one, require launchpad.Edit on the product or distro
1365
2938.2.6 by Brad Bollenbach
merge from rf, resolving conflicts
1366
    def validate(self, data):
4611.6.48 by Graham Binns
Refactored BugTaskEditView._getEditableFeFieldNames() as a cachedproperty.
1367
        """See `LaunchpadFormView`."""
2938.2.1 by Brad Bollenbach
checkpoint
1368
        bugtask = self.context
4285.2.1 by Mark Shuttleworth
Massive renaming of distrorelease to distroseries
1369
        if bugtask.distroseries is not None:
1370
            distro = bugtask.distroseries.distribution
3614.1.88 by Bjorn Tillenius
make sure that the valid_distrotask validator is used also for distrorelease tasks.
1371
        else:
1372
            distro = bugtask.distribution
6368.1.1 by Bjorn Tillenius
don't break when a product name isn't given on +editstatus.
1373
        old_product = bugtask.product
4611.6.14 by Graham Binns
Fixed display of read-only status and importance fields
1374
13506.4.16 by William Grant
Don't crash when retargetting a task to a non-existent SPN.
1375
        new_spn = data.get('sourcepackagename')
1376
        if distro is not None and bugtask.sourcepackagename != new_spn:
4611.6.29 by Graham Binns
Corrected some error-handling issues
1377
            try:
13506.4.16 by William Grant
Don't crash when retargetting a task to a non-existent SPN.
1378
                target = distro
1379
                if new_spn is not None:
1380
                    target = distro.getSourcePackage(new_spn)
1381
                validate_target(bugtask.bug, target)
13506.4.3 by William Grant
and validate_distrotask.
1382
            except IllegalTarget as e:
13506.4.16 by William Grant
Don't crash when retargetting a task to a non-existent SPN.
1383
                # The field validator may have already set an error.
1384
                # Don't clobber it.
1385
                if not self.getFieldError('sourcepackagename'):
1386
                    self.setFieldError('sourcepackagename', e[0])
4611.6.25 by Graham Binns
Fixed some test breakages
1387
6368.1.1 by Bjorn Tillenius
don't break when a product name isn't given on +editstatus.
1388
        new_product = data.get('product')
1389
        if (old_product is None or old_product == new_product or
11411.7.1 by j.c.sackett
Fixed majority of official_malone calls in code-space. Still need to fix templates.
1390
            bugtask.pillar.bug_tracking_usage != ServiceUsage.LAUNCHPAD):
6368.1.1 by Bjorn Tillenius
don't break when a product name isn't given on +editstatus.
1391
            # Either the product wasn't changed, we're dealing with a #
1392
            # distro task, or the bugtask's product doesn't use Launchpad,
1393
            # which means the product can't be changed.
1394
            return
1395
1396
        if new_product is None:
1397
            self.setFieldError('product', 'Enter a project name')
1398
        else:
4611.6.29 by Graham Binns
Corrected some error-handling issues
1399
            try:
13506.4.2 by William Grant
Replace valid_upstreamtask with validate_target everywhere.
1400
                validate_target(bugtask.bug, new_product)
1401
            except IllegalTarget as e:
1402
                self.setFieldError('product', e[0])
2938.2.1 by Brad Bollenbach
checkpoint
1403
4611.6.33 by Graham Binns
Partial refactor of BugTaskEditView.save_action() into updateContextFromData() (which is where the logic really ought to be).
1404
    def updateContextFromData(self, data, context=None):
1405
        """Updates the context object using the submitted form data.
1406
1407
        This method overrides that of LaunchpadEditFormView because of the
1408
        fairly involved thread of logic behind updating some BugTask
1409
        attributes, in particular the status, assignee and bugwatch fields.
1410
        """
1411
        if context is None:
1412
            context = self.context
4611.6.55 by Graham Binns
Renamed context to bugtask (which is more descriptive) in BugTaskEditView.
1413
        bugtask = context
4611.6.33 by Graham Binns
Partial refactor of BugTaskEditView.save_action() into updateContextFromData() (which is where the logic really ought to be).
1414
3554.1.3 by Brad Bollenbach
Fix bug 977 (Commenting on bug should optionally subscribe you) harder, by also adding the checkbox to the status edit forms
1415
        if self.request.form.get('subscribe', False):
5454.1.5 by Tom Berger
record who created each bug subscription, and display the result in the title of the subscriber link
1416
            bugtask.bug.subscribe(self.user, self.user)
3554.1.3 by Brad Bollenbach
Fix bug 977 (Commenting on bug should optionally subscribe you) harder, by also adding the checkbox to the status edit forms
1417
            self.request.response.addNotification(
13045.16.1 by Chris Johnston
Makes text clearer when subscribing to a bug report.
1418
                "You have subscribed to this bug report.")
3554.1.3 by Brad Bollenbach
Fix bug 977 (Commenting on bug should optionally subscribe you) harder, by also adding the checkbox to the status edit forms
1419
3283.3.1 by Brad Bollenbach
create a new branch for bzr integration, to avoid 3 hour merge time
1420
        # Save the field names we extract from the form in a separate
1421
        # list, because we modify this list of names later if the
4611.6.42 by Graham Binns
Comment alterations.
1422
        # bugtask is reassigned to a different product.
4611.6.49 by Graham Binns
More changes to BugTaskEditView per thumper's recommendations.
1423
        field_names = data.keys()
4611.6.17 by Graham Binns
More tweaking; form still doesn't behave as it should
1424
        new_values = data.copy()
4611.6.25 by Graham Binns
Fixed some test breakages
1425
        data_to_apply = data.copy()
2938.2.1 by Brad Bollenbach
checkpoint
1426
4611.6.55 by Graham Binns
Renamed context to bugtask (which is more descriptive) in BugTaskEditView.
1427
        bugtask_before_modification = Snapshot(
1428
            bugtask, providing=providedBy(bugtask))
3112.1.16 by Brad Bollenbach
checkpoint
1429
3455.1.4 by Brad Bollenbach
checkpoint
1430
        # If the user is reassigning an upstream task to a different
1431
        # product, we'll clear out the milestone value, to avoid
1432
        # violating DB constraints that ensure an upstream task can't
1433
        # be assigned to a milestone on a different product.
4611.6.42 by Graham Binns
Comment alterations.
1434
        milestone_cleared = None
4611.6.34 by Graham Binns
The majority of BugTaskEditView.save_action() is now in BugTaskEditView.updateContextFromData(), which is where it ought to be.
1435
        milestone_ignored = False
13479.2.7 by William Grant
Rip them out of browser.bugtask.
1436
        if bugtask.product and bugtask.product != new_values.get("product"):
4611.6.33 by Graham Binns
Partial refactor of BugTaskEditView.save_action() into updateContextFromData() (which is where the logic really ought to be).
1437
            # We clear the milestone value if one was already set. We ignore
3554.1.9 by Brad Bollenbach
Fix bug 6026 (Oops from changing bug's product when milestone is set) to also handle when product is changed and a new milestone set at the same time, and provide a useful feedback message
1438
            # the milestone value if it was currently None, and the user tried
1439
            # to set a milestone value while also changing the product. This
1440
            # allows us to provide slightly clearer feedback messages.
4611.6.55 by Graham Binns
Renamed context to bugtask (which is more descriptive) in BugTaskEditView.
1441
            if bugtask.milestone:
1442
                milestone_cleared = bugtask.milestone
4611.6.34 by Graham Binns
The majority of BugTaskEditView.save_action() is now in BugTaskEditView.updateContextFromData(), which is where it ought to be.
1443
            elif new_values.get('milestone') is not None:
1444
                milestone_ignored = True
3554.1.9 by Brad Bollenbach
Fix bug 6026 (Oops from changing bug's product when milestone is set) to also handle when product is changed and a new milestone set at the same time, and provide a useful feedback message
1445
12622.5.1 by Curtis Hovey
Always remove the bugtask milestone when retargeting the product.
1446
            # Regardless of the user's permission, the milestone
1447
            # must be cleared because the milestone is unique to a product.
1448
            removeSecurityProxy(bugtask).milestone = None
3455.1.4 by Brad Bollenbach
checkpoint
1449
            # Remove the "milestone" field from the list of fields
1450
            # whose changes we want to apply, because we don't want
1451
            # the form machinery to try and set this value back to
1452
            # what it was!
12622.5.4 by Curtis Hovey
use pop() with the default arg to remove the milestone key if it exisits.
1453
            data_to_apply.pop('milestone', None)
3112.1.22 by Brad Bollenbach
fix bug 6026 (Oops from changing bug's product when milestone is set)
1454
13479.2.7 by William Grant
Rip them out of browser.bugtask.
1455
3455.1.4 by Brad Bollenbach
checkpoint
1456
        # We special case setting assignee and status, because there's
1457
        # a workflow associated with changes to these fields.
4611.6.25 by Graham Binns
Fixed some test breakages
1458
        if "assignee" in data_to_apply:
1459
            del data_to_apply["assignee"]
1460
        if "status" in data_to_apply:
1461
            del data_to_apply["status"]
1462
4611.6.55 by Graham Binns
Renamed context to bugtask (which is more descriptive) in BugTaskEditView.
1463
        # We grab the comment_on_change field before we update bugtask so as
4611.6.34 by Graham Binns
The majority of BugTaskEditView.save_action() is now in BugTaskEditView.updateContextFromData(), which is where it ought to be.
1464
        # to avoid problems accessing the field if the user has changed the
1465
        # product of the BugTask.
1466
        comment_on_change = self.request.form.get(
1467
            "%s.comment_on_change" % self.prefix)
1468
6138.3.3 by Bjorn Tillenius
fix lint warnings. add comment.
1469
        changed = formlib.form.applyChanges(
4611.6.55 by Graham Binns
Renamed context to bugtask (which is more descriptive) in BugTaskEditView.
1470
            bugtask, self.form_fields, data_to_apply, self.adapters)
4611.6.23 by Graham Binns
Refactored to use LaunchpadEditView
1471
4611.6.34 by Graham Binns
The majority of BugTaskEditView.save_action() is now in BugTaskEditView.updateContextFromData(), which is where it ought to be.
1472
        # Now that we've updated the bugtask we can add messages about
1473
        # milestone changes, if there were any.
1474
        if milestone_cleared:
1475
            self.request.response.addWarningNotification(
1476
                "The %s milestone setting has been removed because "
1477
                "you reassigned the bug to %s." % (
1478
                    milestone_cleared.displayname,
4611.6.55 by Graham Binns
Renamed context to bugtask (which is more descriptive) in BugTaskEditView.
1479
                    bugtask.bugtargetdisplayname))
4611.6.34 by Graham Binns
The majority of BugTaskEditView.save_action() is now in BugTaskEditView.updateContextFromData(), which is where it ought to be.
1480
        elif milestone_ignored:
1481
            self.request.response.addWarningNotification(
1482
                "The milestone setting was ignored because "
4785.3.6 by Jeroen Vermeulen
Removed whitespace from ends of lines.
1483
                "you reassigned the bug to %s." %
4611.6.55 by Graham Binns
Renamed context to bugtask (which is more descriptive) in BugTaskEditView.
1484
                bugtask.bugtargetdisplayname)
4611.6.34 by Graham Binns
The majority of BugTaskEditView.save_action() is now in BugTaskEditView.updateContextFromData(), which is where it ought to be.
1485
1486
        if comment_on_change:
4611.6.55 by Graham Binns
Renamed context to bugtask (which is more descriptive) in BugTaskEditView.
1487
            bugtask.bug.newMessage(
4611.6.34 by Graham Binns
The majority of BugTaskEditView.save_action() is now in BugTaskEditView.updateContextFromData(), which is where it ought to be.
1488
                owner=getUtility(ILaunchBag).user,
4611.6.55 by Graham Binns
Renamed context to bugtask (which is more descriptive) in BugTaskEditView.
1489
                subject=bugtask.bug.followup_subject(),
4611.6.34 by Graham Binns
The majority of BugTaskEditView.save_action() is now in BugTaskEditView.updateContextFromData(), which is where it ought to be.
1490
                content=comment_on_change)
1491
3554.1.21 by Brad Bollenbach
fix assign to nobody on bugtask edit form
1492
        # Set the "changed" flag properly, just in case status and/or assignee
1493
        # happen to be the only values that changed. We explicitly verify that
5282.1.4 by Tom Berger
change the comment to give a better explanation of why the fields may be missing.
1494
        # we got a new status and/or assignee, because the form is not always
1495
        # guaranteed to pass all the values. For example: bugtasks linked to a
13402.4.1 by Graham Binns
I can't quite believe this, but the tests pass.
1496
        # bug watch don't allow editing the form, and the value is missing
5282.1.4 by Tom Berger
change the comment to give a better explanation of why the fields may be missing.
1497
        # from the form.
5282.1.2 by Tom Berger
some review suggestions from BjornT and barry.
1498
        missing = object()
1499
        new_status = new_values.pop("status", missing)
12641.6.18 by Ian Booth
Use javascript validation for assignment to non contributor
1500
        new_assignee = new_values.pop("assignee", missing)
5282.1.3 by Tom Berger
post-review changes from barry.
1501
        if new_status is not missing and bugtask.status != new_status:
3455.1.4 by Brad Bollenbach
checkpoint
1502
            changed = True
13402.4.5 by Graham Binns
Tweaked according to Rob's requirements.
1503
            try:
1504
                bugtask.transitionToStatus(new_status, self.user)
1505
            except UserCannotEditBugTaskStatus:
1506
                # We need to roll back the transaction at this point,
1507
                # since other changes may have been made.
1508
                transaction.abort()
1509
                self.setFieldError(
1510
                    'status',
1511
                    "Only the Bug Supervisor for %s can set the bug's "
1512
                    "status to %s" %
1513
                    (bugtask.target.displayname, new_status.title))
1514
                return
3554.1.21 by Brad Bollenbach
fix assign to nobody on bugtask edit form
1515
5282.1.3 by Tom Berger
post-review changes from barry.
1516
        if new_assignee is not missing and bugtask.assignee != new_assignee:
12641.6.18 by Ian Booth
Use javascript validation for assignment to non contributor
1517
            if new_assignee is not None and new_assignee != self.user:
1518
                is_contributor = new_assignee.isBugContributorInTarget(
1519
                    user=self.user, target=bugtask.pillar)
1520
                if not is_contributor:
1521
                    # If we have a new assignee who isn't a bug
1522
                    # contributor in this pillar, we display a warning
1523
                    # to the user, in case they made a mistake.
1524
                    self.request.response.addWarningNotification(
1525
                        structured(
1526
                        """<a href="%s">%s</a>
1527
                        did not previously have any assigned bugs in
1528
                        <a href="%s">%s</a>.
1529
                        <br /><br />
1530
                        If this bug was assigned by mistake,
1531
                        you may <a href="%s/+editstatus"
13512.1.1 by William Grant
Fix lack of escaping in non-AJAX bugtask assignee warning.
1532
                        >change the assignment</a>.""",
12641.6.18 by Ian Booth
Use javascript validation for assignment to non contributor
1533
                        canonical_url(new_assignee),
1534
                        new_assignee.displayname,
1535
                        canonical_url(bugtask.pillar),
1536
                        bugtask.pillar.title,
13512.1.1 by William Grant
Fix lack of escaping in non-AJAX bugtask assignee warning.
1537
                        canonical_url(bugtask)))
5821.2.66 by James Henstridge
If you transition the bug task assignee before checking whether the new
1538
            changed = True
1539
            bugtask.transitionToAssignee(new_assignee)
5282.1.1 by Tom Berger
don't ask for confirmation, just display a notification.
1540
4611.6.55 by Graham Binns
Renamed context to bugtask (which is more descriptive) in BugTaskEditView.
1541
        if bugtask_before_modification.bugwatch != bugtask.bugwatch:
6602.5.13 by Tom Berger
when resetting a bugtask to use a bugwatch, use the bug importer
1542
            bug_importer = getUtility(ILaunchpadCelebrities).bug_importer
4611.6.55 by Graham Binns
Renamed context to bugtask (which is more descriptive) in BugTaskEditView.
1543
            if bugtask.bugwatch is None:
3270.3.54 by Matthew Paul Thomas
merges rocketfuel, resolving 6 conflicts
1544
                # Reset the status and importance to the default values,
3498.4.20 by Bjorn Tillenius
reset status and severity to default values if the bug watch is unlinked.
1545
                # since Unknown isn't selectable in the UI.
4611.6.55 by Graham Binns
Renamed context to bugtask (which is more descriptive) in BugTaskEditView.
1546
                bugtask.transitionToStatus(
6602.5.12 by Tom Berger
when resetting a bugtask to use a bugwatch, use the bug importer
1547
                    IBugTask['status'].default, bug_importer)
6602.5.3 by Tom Berger
convert all assingments to bugtask.importance to calls to transitionToImportance
1548
                bugtask.transitionToImportance(
6602.5.12 by Tom Berger
when resetting a bugtask to use a bugwatch, use the bug importer
1549
                    IBugTask['importance'].default, bug_importer)
3498.4.20 by Bjorn Tillenius
reset status and severity to default values if the bug watch is unlinked.
1550
            else:
4664.1.1 by Curtis Hovey
Normalized comments for bug 3732.
1551
                #XXX: Bjorn Tillenius 2006-03-01:
1552
                #     Reset the bug task's status information. The right
3498.4.20 by Bjorn Tillenius
reset status and severity to default values if the bug watch is unlinked.
1553
                #     thing would be to convert the bug watch's status to a
4168.2.3 by Francis J. Lacoste
More Malone references removal
1554
                #     Launchpad status, but it's not trivial to do at the
3498.4.20 by Bjorn Tillenius
reset status and severity to default values if the bug watch is unlinked.
1555
                #     moment. I will fix this later.
4611.6.55 by Graham Binns
Renamed context to bugtask (which is more descriptive) in BugTaskEditView.
1556
                bugtask.transitionToStatus(
11411.7.29 by j.c.sackett
Lint fixes.
1557
                    BugTaskStatus.UNKNOWN,
1558
                    bug_importer)
6602.5.3 by Tom Berger
convert all assingments to bugtask.importance to calls to transitionToImportance
1559
                bugtask.transitionToImportance(
11411.7.29 by j.c.sackett
Lint fixes.
1560
                    BugTaskImportance.UNKNOWN,
1561
                    bug_importer)
4611.6.55 by Graham Binns
Renamed context to bugtask (which is more descriptive) in BugTaskEditView.
1562
                bugtask.transitionToAssignee(None)
3063.2.34 by Bjorn Tillenius
add some doctests for +editstatus. make sure status information gets reset on bugwatch changes.
1563
2938.2.1 by Brad Bollenbach
checkpoint
1564
        if changed:
4611.6.35 by Graham Binns
Removed extraneous _missing_value attribute.
1565
            # We only set the statusexplanation field to the value of the
1566
            # change comment if the BugTask has actually been changed in some
1567
            # way. Otherwise, we just leave it as a comment on the bug.
4611.6.34 by Graham Binns
The majority of BugTaskEditView.save_action() is now in BugTaskEditView.updateContextFromData(), which is where it ought to be.
1568
            if comment_on_change:
4611.6.55 by Graham Binns
Renamed context to bugtask (which is more descriptive) in BugTaskEditView.
1569
                bugtask.statusexplanation = comment_on_change
4611.6.34 by Graham Binns
The majority of BugTaskEditView.save_action() is now in BugTaskEditView.updateContextFromData(), which is where it ought to be.
1570
            else:
4611.6.55 by Graham Binns
Renamed context to bugtask (which is more descriptive) in BugTaskEditView.
1571
                bugtask.statusexplanation = ""
4611.6.34 by Graham Binns
The majority of BugTaskEditView.save_action() is now in BugTaskEditView.updateContextFromData(), which is where it ought to be.
1572
2938.2.1 by Brad Bollenbach
checkpoint
1573
            notify(
7876.3.6 by Francis J. Lacoste
Used ISQLObject from lazr.lifecycle
1574
                ObjectModifiedEvent(
4611.6.55 by Graham Binns
Renamed context to bugtask (which is more descriptive) in BugTaskEditView.
1575
                    object=bugtask,
1576
                    object_before_modification=bugtask_before_modification,
3254.1.24 by Bjorn Tillenius
fix bug 25724, remove comment_on_change hack.
1577
                    edited_fields=field_names))
2938.2.1 by Brad Bollenbach
checkpoint
1578
4611.6.55 by Graham Binns
Renamed context to bugtask (which is more descriptive) in BugTaskEditView.
1579
        if bugtask.sourcepackagename is not None:
1580
            real_package_name = bugtask.sourcepackagename.name
4611.6.49 by Graham Binns
More changes to BugTaskEditView per thumper's recommendations.
1581
1582
            # We get entered_package_name directly from the form here, since
1583
            # validating the sourcepackagename field mutates its value in to
1584
            # the one already in real_package_name, which makes our comparison
1585
            # of the two below useless.
4611.6.37 by Graham Binns
Fixed a broken doctest.
1586
            entered_package_name = self.request.form.get(
1587
                self.widgets['sourcepackagename'].name)
4611.6.49 by Graham Binns
More changes to BugTaskEditView per thumper's recommendations.
1588
3691.142.2 by Bjorn Tillenius
display a notification when a binary package is mapped to a source package.
1589
            if real_package_name != entered_package_name:
1590
                # The user entered a binary package name which got
1591
                # mapped to a source package.
1592
                self.request.response.addNotification(
3691.180.3 by Diogo Matsubara
Fixes bug 69459 (Notification about binary->source filing is a run-on sentence)
1593
                    "'%(entered_package)s' is a binary package. This bug has"
3691.142.2 by Bjorn Tillenius
display a notification when a binary package is mapped to a source package.
1594
                    " been assigned to its source package '%(real_package)s'"
4611.6.37 by Graham Binns
Fixed a broken doctest.
1595
                    " instead." %
1596
                    {'entered_package': entered_package_name,
1597
                     'real_package': real_package_name})
3691.142.2 by Bjorn Tillenius
display a notification when a binary package is mapped to a source package.
1598
4611.6.55 by Graham Binns
Renamed context to bugtask (which is more descriptive) in BugTaskEditView.
1599
        if (bugtask_before_modification.sourcepackagename !=
1600
            bugtask.sourcepackagename):
3153.1.5 by Brad Bollenbach
checkpoint
1601
            # The source package was changed, so tell the user that we've
6087.7.2 by Tom Berger
convert bug contact to bug supervisor in template and browser code
1602
            # subscribed the new bug supervisors.
3153.1.5 by Brad Bollenbach
checkpoint
1603
            self.request.response.addNotification(
6025.3.1 by Tom Berger
rename bug contact to bug supervisor in the UI and test narrative
1604
                "The bug supervisor for %s has been subscribed to this bug."
4974.2.1 by Curtis Hovey
Fixes per pylint.
1605
                 % (bugtask.bugtargetdisplayname))
4611.6.33 by Graham Binns
Partial refactor of BugTaskEditView.save_action() into updateContextFromData() (which is where the logic really ought to be).
1606
1607
    @action('Save Changes', name='save')
1608
    def save_action(self, action, data):
4974.2.1 by Curtis Hovey
Fixes per pylint.
1609
        """Update the bugtask with the form data."""
4611.6.33 by Graham Binns
Partial refactor of BugTaskEditView.save_action() into updateContextFromData() (which is where the logic really ought to be).
1610
        self.updateContextFromData(data)
3153.1.5 by Brad Bollenbach
checkpoint
1611
13402.4.8 by Graham Binns
Cleaned up lint.
1612
3063.2.26 by Bjorn Tillenius
fix so that +viewstatus works.
1613
class BugTaskStatusView(LaunchpadView):
1614
    """Viewing the status of a bug task."""
1615
11118.3.1 by Curtis Hovey
Removed BugTaskSOP, inlined page_titles, updated page_titles for specialised bugtask
1616
    page_title = 'View status'
1617
3063.2.26 by Bjorn Tillenius
fix so that +viewstatus works.
1618
    def initialize(self):
3063.2.37 by Bjorn Tillenius
doctweaks
1619
        """Set up the appropriate widgets.
1620
1621
        Different widgets are shown depending on if it's a remote bug
1622
        task or not.
1623
        """
3063.2.26 by Bjorn Tillenius
fix so that +viewstatus works.
1624
        field_names = [
3270.3.10 by Matthew Paul Thomas
Changes BugTaskSeverity to BugTaskImportance.
1625
            'status', 'importance', 'assignee', 'statusexplanation']
3024.1.74 by Christian Reis
Remove IRemoteBugTask marker interface; it deoptimizes BugTask._init() and can mostly be replaced with a property
1626
        if not self.context.target_uses_malone:
3063.2.26 by Bjorn Tillenius
fix so that +viewstatus works.
1627
            field_names += ['bugwatch']
1628
            self.milestone_widget = None
1629
        else:
1630
            field_names += ['milestone']
1631
            self.bugwatch_widget = None
1632
13479.2.7 by William Grant
Rip them out of browser.bugtask.
1633
        if self.context.distroseries or self.context.distribution:
3485.6.1 by Brad Bollenbach
recreate branch from scratch
1634
            field_names += ['sourcepackagename']
3063.2.26 by Bjorn Tillenius
fix so that +viewstatus works.
1635
1636
        self.assignee_widget = CustomWidgetFactory(AssigneeDisplayWidget)
1637
        self.status_widget = CustomWidgetFactory(DBItemDisplayWidget)
3270.3.10 by Matthew Paul Thomas
Changes BugTaskSeverity to BugTaskImportance.
1638
        self.importance_widget = CustomWidgetFactory(DBItemDisplayWidget)
3063.2.26 by Bjorn Tillenius
fix so that +viewstatus works.
1639
1640
        setUpWidgets(self, IBugTask, IDisplayWidget, names=field_names)
1641
1642
3691.141.11 by kiko
UnXXX the render_bugtask_status functions; move it into a BugTaskListingView, which isn't the best name but which at least speaks of its legacy.
1643
class BugTaskListingView(LaunchpadView):
1644
    """A view designed for displaying bug tasks in lists."""
1645
    # Note that this right now is only used in tests and to render
1646
    # status in the CVEReportView. It may be a candidate for refactoring
1647
    # or removal.
1648
    @property
1649
    def status(self):
4974.2.1 by Curtis Hovey
Fixes per pylint.
1650
        """Return an HTML representation of the bugtask status.
4974.2.2 by Curtis Hovey
Removed trailing whitespace.
1651
4974.2.1 by Curtis Hovey
Fixes per pylint.
1652
        The assignee is included.
1653
        """
3691.141.11 by kiko
UnXXX the render_bugtask_status functions; move it into a BugTaskListingView, which isn't the best name but which at least speaks of its legacy.
1654
        bugtask = self.context
1655
        assignee = bugtask.assignee
1656
        status = bugtask.status
1657
        status_title = status.title.capitalize()
1658
1659
        if not assignee:
1660
            return status_title + ' (unassigned)'
4005.1.10 by Mark Shuttleworth
Test fixes for new branding, and initial test for team branding
1661
        assignee_html = PersonFormatterAPI(assignee).link('+assignedbugs')
3691.141.11 by kiko
UnXXX the render_bugtask_status functions; move it into a BugTaskListingView, which isn't the best name but which at least speaks of its legacy.
1662
4621.4.2 by Tom Berger
interim solution
1663
        if status in (BugTaskStatus.INVALID,
1664
                      BugTaskStatus.FIXCOMMITTED):
3691.141.11 by kiko
UnXXX the render_bugtask_status functions; move it into a BugTaskListingView, which isn't the best name but which at least speaks of its legacy.
1665
            return '%s by %s' % (status_title, assignee_html)
1666
        else:
1667
            return '%s, assigned to %s' % (status_title, assignee_html)
1668
1669
    @property
1670
    def status_elsewhere(self):
1671
        """Return human-readable representation of the status of this bug
1672
        in other contexts for which it's reported.
1673
        """
1674
        bugtask = self.context
1675
        related_tasks = bugtask.related_tasks
1676
        if not related_tasks:
1677
            return "not filed elsewhere"
1678
1679
        fixes_found = len(
1680
            [task for task in related_tasks
1681
             if task.status in (BugTaskStatus.FIXCOMMITTED,
1682
                                BugTaskStatus.FIXRELEASED)])
1683
        if fixes_found:
1684
            return "fixed in %d of %d places" % (
1685
                fixes_found, len(bugtask.bug.bugtasks))
1686
        elif len(related_tasks) == 1:
1687
            return "filed in 1 other place"
1688
        else:
1689
            return "filed in %d other places" % len(related_tasks)
1690
1691
    def render(self):
1692
        """Make rendering this template-less view not crash."""
1693
        return u""
1694
1695
9570.15.4 by Gavin Panella
Split out the *_info properties into a mixin class.
1696
class BugsInfoMixin:
9570.15.18 by Gavin Panella
Split the BugsInfoMixin.*_count properties into a subclass, BugStatsMixin.
1697
    """Contains properties giving URLs to bug information."""
9570.15.17 by Gavin Panella
Split the BugsInfoMixin *_info properties into component properties. This is so other pages can ignore expensive to calculate properties if required.
1698
1699
    @property
1700
    def bugs_fixed_elsewhere_url(self):
1701
        """A URL to a list of bugs fixed elsewhere."""
1702
        return "%s?field.status_upstream=resolved_upstream" % (
1703
            canonical_url(self.context, view_name='+bugs'))
1704
1705
    @property
1706
    def open_cve_bugs_url(self):
1707
        """A URL to a list of open bugs linked to CVEs."""
1708
        return "%s?field.has_cve=on" % (
9570.15.15 by Gavin Panella
Remove all label keys in the info dicts; they're not used. Also, use view_name with canonical_url(), and remove unnecessary locals vars.
1709
            canonical_url(self.context, view_name='+bugs'))
9570.15.1 by Gavin Panella
Put the new bug stats stuff into the asynchronously loaded bugfilters portlet.
1710
1711
    @property
9570.15.17 by Gavin Panella
Split the BugsInfoMixin *_info properties into component properties. This is so other pages can ignore expensive to calculate properties if required.
1712
    def open_cve_bugs_has_report(self):
1713
        """Whether or not the context has a CVE report page."""
1714
        return queryMultiAdapter(
9570.15.15 by Gavin Panella
Remove all label keys in the info dicts; they're not used. Also, use view_name with canonical_url(), and remove unnecessary locals vars.
1715
            (self.context, self.request), name='+cve') is not None
9570.15.17 by Gavin Panella
Split the BugsInfoMixin *_info properties into component properties. This is so other pages can ignore expensive to calculate properties if required.
1716
1717
    @property
1718
    def pending_bugwatches_url(self):
1719
        """A URL to a list of bugs that need a bugwatch.
1720
1721
        None is returned if the context is not an upstream product.
1722
        """
1723
        if not IProduct.providedBy(self.context):
1724
            return None
11411.7.1 by j.c.sackett
Fixed majority of official_malone calls in code-space. Still need to fix templates.
1725
        if self.context.bug_tracking_usage == ServiceUsage.LAUNCHPAD:
9570.15.17 by Gavin Panella
Split the BugsInfoMixin *_info properties into component properties. This is so other pages can ignore expensive to calculate properties if required.
1726
            return None
1727
        return "%s?field.status_upstream=pending_bugwatch" % (
1728
            canonical_url(self.context, view_name='+bugs'))
1729
1730
    @property
9570.15.18 by Gavin Panella
Split the BugsInfoMixin.*_count properties into a subclass, BugStatsMixin.
1731
    def expirable_bugs_url(self):
1732
        """A URL to a list of bugs that can expire, or None.
1733
1734
        If the bugtarget is not a supported implementation, or its pillar
1735
        does not have enable_bug_expiration set to True, None is returned.
1736
        The bugtarget may be an `IDistribution`, `IDistroSeries`, `IProduct`,
1737
        or `IProductSeries`.
1738
        """
1739
        if target_has_expirable_bugs_listing(self.context):
1740
            return canonical_url(self.context, view_name='+expirable-bugs')
1741
        else:
1742
            return None
1743
1744
    @property
1745
    def new_bugs_url(self):
1746
        """A URL to a page of new bugs."""
1747
        return get_buglisting_search_filter_url(
1748
            status=BugTaskStatus.NEW.title)
1749
1750
    @property
10224.24.1 by Martin Pool
Adds a count of inprogress and high-priority bugs into the count portlet.
1751
    def inprogress_bugs_url(self):
1752
        """A URL to a page of inprogress bugs."""
1753
        return get_buglisting_search_filter_url(
1754
            status=BugTaskStatus.INPROGRESS.title)
1755
1756
    @property
9570.15.18 by Gavin Panella
Split the BugsInfoMixin.*_count properties into a subclass, BugStatsMixin.
1757
    def open_bugs_url(self):
1758
        """A URL to a list of open bugs."""
1759
        return canonical_url(self.context, view_name='+bugs')
1760
1761
    @property
1762
    def critical_bugs_url(self):
1763
        """A URL to a list of critical bugs."""
1764
        return get_buglisting_search_filter_url(
1765
            status=[status.title for status in UNRESOLVED_BUGTASK_STATUSES],
1766
            importance=BugTaskImportance.CRITICAL.title)
1767
1768
    @property
10224.24.1 by Martin Pool
Adds a count of inprogress and high-priority bugs into the count portlet.
1769
    def high_bugs_url(self):
1770
        """A URL to a list of high priority bugs."""
1771
        return get_buglisting_search_filter_url(
1772
            status=[status.title for status in UNRESOLVED_BUGTASK_STATUSES],
1773
            importance=BugTaskImportance.HIGH.title)
1774
1775
    @property
9570.15.18 by Gavin Panella
Split the BugsInfoMixin.*_count properties into a subclass, BugStatsMixin.
1776
    def my_bugs_url(self):
1777
        """A URL to a list of bugs assigned to the user, or None."""
1778
        if self.user is None:
1779
            return None
1780
        else:
1781
            return get_buglisting_search_filter_url(assignee=self.user.name)
1782
12449.3.1 by Jonathan Lange
Add a link to show bugs reported by me.
1783
    @property
1784
    def my_reported_bugs_url(self):
1785
        """A URL to a list of bugs reported by the user, or None."""
1786
        if self.user is None:
1787
            return None
12449.3.3 by Jonathan Lange
Correctly search for bugs reported by me ('bug_reporter', not 'reporter')
1788
        return get_buglisting_search_filter_url(bug_reporter=self.user.name)
12449.3.1 by Jonathan Lange
Add a link to show bugs reported by me.
1789
9570.15.18 by Gavin Panella
Split the BugsInfoMixin.*_count properties into a subclass, BugStatsMixin.
1790
1791
class BugsStatsMixin(BugsInfoMixin):
1792
    """Contains properties giving bug stats.
1793
1794
    These can be expensive to obtain.
1795
    """
1796
12599.4.2 by Leonard Richardson
Merge from trunk.
1797
    @cachedproperty
1798
    def _bug_stats(self):
7675.1204.8 by Robert Collins
And port the bug stats portlet to countBugs2.
1799
        # Circular fail.
1800
        from lp.bugs.model.bugsummary import BugSummary
12599.4.2 by Leonard Richardson
Merge from trunk.
1801
        bug_task_set = getUtility(IBugTaskSet)
7675.1204.8 by Robert Collins
And port the bug stats portlet to countBugs2.
1802
        groups = (BugSummary.status, BugSummary.importance,
1803
            BugSummary.has_patch, BugSummary.fixed_upstream)
7675.1204.9 by Robert Collins
replace countBugs with countBugs2.
1804
        counts = bug_task_set.countBugs(self.user, [self.context], groups)
12599.4.2 by Leonard Richardson
Merge from trunk.
1805
        # Sum the split out aggregates.
1806
        new = 0
1807
        open = 0
1808
        inprogress = 0
1809
        critical = 0
1810
        high = 0
1811
        with_patch = 0
1812
        resolved_upstream = 0
1813
        for metadata, count in counts.items():
1814
            status = metadata[0]
1815
            importance = metadata[1]
1816
            has_patch = metadata[2]
1817
            was_resolved_upstream = metadata[3]
1818
            if status == BugTaskStatus.NEW:
1819
                new += count
1820
            elif status == BugTaskStatus.INPROGRESS:
1821
                inprogress += count
1822
            if importance == BugTaskImportance.CRITICAL:
1823
                critical += count
1824
            elif importance == BugTaskImportance.HIGH:
1825
                high += count
1826
            if has_patch and DISPLAY_BUG_STATUS_FOR_PATCHES[status]:
1827
                with_patch += count
1828
            if was_resolved_upstream:
1829
                resolved_upstream += count
1830
            open += count
1831
        result = dict(new=new, open=open, inprogress=inprogress, high=high,
1832
            critical=critical, with_patch=with_patch,
1833
            resolved_upstream=resolved_upstream)
1834
        return result
1835
9570.15.18 by Gavin Panella
Split the BugsInfoMixin.*_count properties into a subclass, BugStatsMixin.
1836
    @property
1837
    def bugs_fixed_elsewhere_count(self):
1838
        """A count of bugs fixed elsewhere."""
12599.4.2 by Leonard Richardson
Merge from trunk.
1839
        return self._bug_stats['resolved_upstream']
9570.15.18 by Gavin Panella
Split the BugsInfoMixin.*_count properties into a subclass, BugStatsMixin.
1840
1841
    @property
1842
    def open_cve_bugs_count(self):
1843
        """A count of open bugs linked to CVEs."""
9570.15.20 by Gavin Panella
Most callers of get_default_search_params() do not care about ordering.
1844
        params = get_default_search_params(self.user)
9570.15.18 by Gavin Panella
Split the BugsInfoMixin.*_count properties into a subclass, BugStatsMixin.
1845
        params.has_cve = True
1846
        return self.context.searchTasks(params).count()
1847
1848
    @property
9570.15.17 by Gavin Panella
Split the BugsInfoMixin *_info properties into component properties. This is so other pages can ignore expensive to calculate properties if required.
1849
    def pending_bugwatches_count(self):
1850
        """A count of bugs that need a bugwatch.
1851
1852
        None is returned if the context is not an upstream product.
9570.15.1 by Gavin Panella
Put the new bug stats stuff into the asynchronously loaded bugfilters portlet.
1853
        """
1854
        if not IProduct.providedBy(self.context):
1855
            return None
11411.7.1 by j.c.sackett
Fixed majority of official_malone calls in code-space. Still need to fix templates.
1856
        if self.context.bug_tracking_usage == ServiceUsage.LAUNCHPAD:
9570.15.1 by Gavin Panella
Put the new bug stats stuff into the asynchronously loaded bugfilters portlet.
1857
            return None
9570.15.20 by Gavin Panella
Most callers of get_default_search_params() do not care about ordering.
1858
        params = get_default_search_params(self.user)
9570.15.1 by Gavin Panella
Put the new bug stats stuff into the asynchronously loaded bugfilters portlet.
1859
        params.pending_bugwatch_elsewhere = True
9570.15.17 by Gavin Panella
Split the BugsInfoMixin *_info properties into component properties. This is so other pages can ignore expensive to calculate properties if required.
1860
        return self.context.searchTasks(params).count()
1861
1862
    @property
1863
    def expirable_bugs_count(self):
1864
        """A count of bugs that can expire, or None.
1865
1866
        If the bugtarget is not a supported implementation, or its pillar
1867
        does not have enable_bug_expiration set to True, None is returned.
1868
        The bugtarget may be an `IDistribution`, `IDistroSeries`, `IProduct`,
1869
        or `IProductSeries`.
1870
        """
11057.8.2 by Brian Murray
modify can_expire to use the days_before_expiration config option
1871
        days_old = config.malone.days_before_expiration
1872
9570.15.17 by Gavin Panella
Split the BugsInfoMixin *_info properties into component properties. This is so other pages can ignore expensive to calculate properties if required.
1873
        if target_has_expirable_bugs_listing(self.context):
1874
            return getUtility(IBugTaskSet).findExpirableBugTasks(
11057.8.2 by Brian Murray
modify can_expire to use the days_before_expiration config option
1875
                days_old, user=self.user, target=self.context).count()
9570.15.17 by Gavin Panella
Split the BugsInfoMixin *_info properties into component properties. This is so other pages can ignore expensive to calculate properties if required.
1876
        else:
1877
            return None
1878
1879
    @property
1880
    def new_bugs_count(self):
1881
        """A count of new bugs."""
12599.4.2 by Leonard Richardson
Merge from trunk.
1882
        return self._bug_stats['new']
9570.15.17 by Gavin Panella
Split the BugsInfoMixin *_info properties into component properties. This is so other pages can ignore expensive to calculate properties if required.
1883
1884
    @property
1885
    def open_bugs_count(self):
1886
        """A count of open bugs."""
12599.4.2 by Leonard Richardson
Merge from trunk.
1887
        return self._bug_stats['open']
9570.15.17 by Gavin Panella
Split the BugsInfoMixin *_info properties into component properties. This is so other pages can ignore expensive to calculate properties if required.
1888
1889
    @property
10224.24.1 by Martin Pool
Adds a count of inprogress and high-priority bugs into the count portlet.
1890
    def inprogress_bugs_count(self):
1891
        """A count of in-progress bugs."""
12599.4.2 by Leonard Richardson
Merge from trunk.
1892
        return self._bug_stats['inprogress']
10224.24.1 by Martin Pool
Adds a count of inprogress and high-priority bugs into the count portlet.
1893
1894
    @property
9570.15.17 by Gavin Panella
Split the BugsInfoMixin *_info properties into component properties. This is so other pages can ignore expensive to calculate properties if required.
1895
    def critical_bugs_count(self):
1896
        """A count of critical bugs."""
12599.4.2 by Leonard Richardson
Merge from trunk.
1897
        return self._bug_stats['critical']
9570.15.17 by Gavin Panella
Split the BugsInfoMixin *_info properties into component properties. This is so other pages can ignore expensive to calculate properties if required.
1898
1899
    @property
10224.24.1 by Martin Pool
Adds a count of inprogress and high-priority bugs into the count portlet.
1900
    def high_bugs_count(self):
1901
        """A count of high priority bugs."""
12599.4.2 by Leonard Richardson
Merge from trunk.
1902
        return self._bug_stats['high']
10224.24.1 by Martin Pool
Adds a count of inprogress and high-priority bugs into the count portlet.
1903
1904
    @property
9570.15.17 by Gavin Panella
Split the BugsInfoMixin *_info properties into component properties. This is so other pages can ignore expensive to calculate properties if required.
1905
    def my_bugs_count(self):
1906
        """A count of bugs assigned to the user, or None."""
1907
        if self.user is None:
1908
            return None
1909
        else:
9570.15.20 by Gavin Panella
Most callers of get_default_search_params() do not care about ordering.
1910
            params = get_default_search_params(self.user)
9570.15.17 by Gavin Panella
Split the BugsInfoMixin *_info properties into component properties. This is so other pages can ignore expensive to calculate properties if required.
1911
            params.assignee = self.user
1912
            return self.context.searchTasks(params).count()
9570.15.1 by Gavin Panella
Put the new bug stats stuff into the asynchronously loaded bugfilters portlet.
1913
7675.533.1 by Karl Fogel
Resolve Bug #255868: link to "+patches" view from product bugs page.
1914
    @property
12449.3.1 by Jonathan Lange
Add a link to show bugs reported by me.
1915
    def my_reported_bugs_count(self):
1916
        """A count of bugs reported by the user, or None."""
1917
        if self.user is None:
1918
            return None
1919
        params = get_default_search_params(self.user)
12449.3.4 by Jonathan Lange
Show the count correctly.
1920
        params.bug_reporter = self.user
12449.3.1 by Jonathan Lange
Add a link to show bugs reported by me.
1921
        return self.context.searchTasks(params).count()
1922
1923
    @property
7675.533.1 by Karl Fogel
Resolve Bug #255868: link to "+patches" view from product bugs page.
1924
    def bugs_with_patches_count(self):
1925
        """A count of unresolved bugs with patches."""
12599.4.2 by Leonard Richardson
Merge from trunk.
1926
        return self._bug_stats['with_patch']
7675.533.1 by Karl Fogel
Resolve Bug #255868: link to "+patches" view from product bugs page.
1927
3979.1.4 by Guilherme Salgado
Add a new +series page for IProducts, plus a few refactorings
1928
9570.15.19 by Gavin Panella
Make bugtarget-portlet-bugfilters-content.pt available via two views, one with stats, one without.
1929
class BugListingPortletInfoView(LaunchpadView, BugsInfoMixin):
1930
    """Portlet containing available bug listings without stats."""
1931
1932
1933
class BugListingPortletStatsView(LaunchpadView, BugsStatsMixin):
1934
    """Portlet containing available bug listings with stats."""
9570.15.4 by Gavin Panella
Split out the *_info properties into a mixin class.
1935
1936
3979.1.4 by Guilherme Salgado
Add a new +series page for IProducts, plus a few refactorings
1937
def get_buglisting_search_filter_url(
7675.532.2 by Abel Deuring
upstream report template changed to show the number of bugs having patchs that might be intersting for upstream
1938
        assignee=None, importance=None, status=None, status_upstream=None,
12449.3.3 by Jonathan Lange
Correctly search for bugs reported by me ('bug_reporter', not 'reporter')
1939
        has_patches=None, bug_reporter=None):
3979.1.4 by Guilherme Salgado
Add a new +series page for IProducts, plus a few refactorings
1940
    """Return the given URL with the search parameters specified."""
1941
    search_params = []
1942
6857.1.4 by Graham Binns
get_buglisting_search_filter_url() now takes a status_upstream parameter for convenience.
1943
    if assignee is not None:
3979.1.4 by Guilherme Salgado
Add a new +series page for IProducts, plus a few refactorings
1944
        search_params.append(('field.assignee', assignee))
6857.1.4 by Graham Binns
get_buglisting_search_filter_url() now takes a status_upstream parameter for convenience.
1945
    if importance is not None:
3979.1.4 by Guilherme Salgado
Add a new +series page for IProducts, plus a few refactorings
1946
        search_params.append(('field.importance', importance))
6857.1.4 by Graham Binns
get_buglisting_search_filter_url() now takes a status_upstream parameter for convenience.
1947
    if status is not None:
3979.1.4 by Guilherme Salgado
Add a new +series page for IProducts, plus a few refactorings
1948
        search_params.append(('field.status', status))
6857.1.4 by Graham Binns
get_buglisting_search_filter_url() now takes a status_upstream parameter for convenience.
1949
    if status_upstream is not None:
1950
        search_params.append(('field.status_upstream', status_upstream))
7675.532.2 by Abel Deuring
upstream report template changed to show the number of bugs having patchs that might be intersting for upstream
1951
    if has_patches is not None:
1952
        search_params.append(('field.has_patch', 'on'))
12449.3.3 by Jonathan Lange
Correctly search for bugs reported by me ('bug_reporter', not 'reporter')
1953
    if bug_reporter is not None:
1954
        search_params.append(('field.bug_reporter', bug_reporter))
3979.1.4 by Guilherme Salgado
Add a new +series page for IProducts, plus a few refactorings
1955
1956
    query_string = urllib.urlencode(search_params, doseq=True)
1957
4758.1.3 by Bjorn Tillenius
make the bugfilter portlet point to the right page from a product's Bugs page.
1958
    search_filter_url = "+bugs?search=Search"
6857.1.4 by Graham Binns
get_buglisting_search_filter_url() now takes a status_upstream parameter for convenience.
1959
    if query_string != '':
3979.1.4 by Guilherme Salgado
Add a new +series page for IProducts, plus a few refactorings
1960
        search_filter_url += "&" + query_string
1961
1962
    return search_filter_url
2749 by Canonical.com Patch Queue Manager
r=salgado. first go at pre-defined bug listings. this merge also includes mpt's bug-listings-love, r=bjornt for that one.
1963
3015.1.46 by Brad Bollenbach
checkpoint
1964
2770.1.40 by Guilherme Salgado
A few fixes Bjorn suggested and removing two unused templates.
1965
def getInitialValuesFromSearchParams(search_params, form_schema):
1966
    """Build a dictionary that can be given as initial values to
1967
    setUpWidgets, based on the given search params.
1968
1969
    >>> initial = getInitialValuesFromSearchParams(
1970
    ...     {'status': any(*UNRESOLVED_BUGTASK_STATUSES)}, IBugTaskSearch)
12164.4.2 by Gavin Panella
Fix doctest gotcha.
1971
    >>> for status in initial['status']:
1972
    ...     print status.name
1973
    NEW
1974
    INCOMPLETE
1975
    CONFIRMED
1976
    TRIAGED
1977
    INPROGRESS
1978
    FIXCOMMITTED
2770.1.40 by Guilherme Salgado
A few fixes Bjorn suggested and removing two unused templates.
1979
1980
    >>> initial = getInitialValuesFromSearchParams(
4621.4.2 by Tom Berger
interim solution
1981
    ...     {'status': BugTaskStatus.INVALID}, IBugTaskSearch)
2770.1.40 by Guilherme Salgado
A few fixes Bjorn suggested and removing two unused templates.
1982
    >>> [status.name for status in initial['status']]
4318.3.3 by Gavin Panella
First round of status renames throughout the tree.
1983
    ['INVALID']
2770.1.40 by Guilherme Salgado
A few fixes Bjorn suggested and removing two unused templates.
1984
1985
    >>> initial = getInitialValuesFromSearchParams(
4911.3.1 by Tom Berger
merge changes from rocketfuel and resolve conflicts
1986
    ...     {'importance': [BugTaskImportance.CRITICAL,
1987
    ...                   BugTaskImportance.HIGH]}, IBugTaskSearch)
3270.3.13 by Matthew Paul Thomas
Makes all except one pagetest pass. Fixes the remaining bug listing forms.
1988
    >>> [importance.name for importance in initial['importance']]
3270.3.48 by Matthew Paul Thomas
Fixes some tests.
1989
    ['CRITICAL', 'HIGH']
2770.1.40 by Guilherme Salgado
A few fixes Bjorn suggested and removing two unused templates.
1990
1991
    >>> getInitialValuesFromSearchParams(
1992
    ...     {'assignee': NULL}, IBugTaskSearch)
1993
    {'assignee': None}
1994
    """
1995
    initial = {}
1996
    for key, value in search_params.items():
1997
        if IList.providedBy(form_schema[key]):
1998
            if isinstance(value, any):
1999
                value = value.query_values
2000
            elif isinstance(value, (list, tuple)):
2001
                value = value
2002
            else:
2003
                value = [value]
2004
        elif value == NULL:
2005
            value = None
2006
        else:
2007
            # Should be safe to pass value as it is to setUpWidgets, no need
2008
            # to worry
2009
            pass
2010
2011
        initial[key] = value
2012
2013
    return initial
2014
3015.1.46 by Brad Bollenbach
checkpoint
2015
3755.4.14 by Tim Penhey
following review comments
2016
class BugTaskListingItem:
2017
    """A decorated bug task.
2018
2019
    Some attributes that we want to display are too convoluted or expensive
2020
    to get on the fly for each bug task in the listing.  These items are
2021
    prefetched by the view and decorate the bug task.
2022
    """
7465.5.1 by Gary Poster
integrate lazr.config and lazr.delegates
2023
    delegates(IBugTask, 'bugtask')
3755.4.14 by Tim Penhey
following review comments
2024
11822.2.1 by Robert Collins
Remove the python code related to mentoring.
2025
    def __init__(self, bugtask, has_bug_branch,
7675.568.3 by Tom Berger
When creating the bug heat flames display, compare to the max heat of the current context, not the target of the bugtask.
2026
                 has_specification, has_patch, request=None,
2027
                 target_context=None):
3755.4.14 by Tim Penhey
following review comments
2028
        self.bugtask = bugtask
4310.1.5 by Bjorn Tillenius
only show the widgets if the user has access to approve the nomination.
2029
        self.review_action_widget = None
6341.4.1 by Bjorn Tillenius
Add an image:badges formatter for BugTaskListingItem.
2030
        self.has_bug_branch = has_bug_branch
2031
        self.has_specification = has_specification
7675.494.1 by Tom Berger
use the bugtask listing item decorator to get the information for the patch badge more efficiently.
2032
        self.has_patch = has_patch
7675.480.1 by Tom Berger
display the bug heat flames in bug listings
2033
        self.request = request
7675.568.3 by Tom Berger
When creating the bug heat flames display, compare to the max heat of the current context, not the target of the bugtask.
2034
        self.target_context = target_context
4334.1.7 by Gavin Panella
Finish widget and get updates working.
2035
9378.4.10 by Curtis Hovey
moved last_significant_change_date to BugTaskListingItem.
2036
    @property
2037
    def last_significant_change_date(self):
2038
        """The date of the last significant change."""
2039
        return (self.bugtask.date_closed or self.bugtask.date_fix_committed or
2040
                self.bugtask.date_inprogress or self.bugtask.date_left_new or
2041
                self.bugtask.datecreated)
2042
7675.480.1 by Tom Berger
display the bug heat flames in bug listings
2043
    @property
2044
    def bug_heat_html(self):
2045
        """Returns the bug heat flames HTML."""
7675.568.3 by Tom Berger
When creating the bug heat flames display, compare to the max heat of the current context, not the target of the bugtask.
2046
        return bugtask_heat_html(self.bugtask, target=self.target_context)
7675.480.1 by Tom Berger
display the bug heat flames in bug listings
2047
3755.4.14 by Tim Penhey
following review comments
2048
3755.4.13 by Tim Penhey
Changed to custom batch navigator for bug listings
2049
class BugListingBatchNavigator(TableBatchNavigator):
3755.4.14 by Tim Penhey
following review comments
2050
    """A specialised batch navigator to load smartly extra bug information."""
4334.1.7 by Gavin Panella
Finish widget and get updates working.
2051
7675.568.3 by Tom Berger
When creating the bug heat flames display, compare to the max heat of the current context, not the target of the bugtask.
2052
    def __init__(self, tasks, request, columns_to_show, size,
2053
                 target_context=None):
11626.3.10 by Curtis Hovey
Hush lints epic complaints about the changes files.
2054
        # XXX sinzui 2009-05-29 bug=381672: Extract the BugTaskListingItem
2055
        # rules to a mixin so that MilestoneView and others can use it.
7675.480.1 by Tom Berger
display the bug heat flames in bug listings
2056
        self.request = request
7675.568.3 by Tom Berger
When creating the bug heat flames display, compare to the max heat of the current context, not the target of the bugtask.
2057
        self.target_context = target_context
3755.4.14 by Tim Penhey
following review comments
2058
        TableBatchNavigator.__init__(
2059
            self, tasks, request, columns_to_show=columns_to_show, size=size)
5958.2.5 by Brad Crittenden
Performance fixes per Kiko
2060
2061
    @cachedproperty
6341.4.1 by Bjorn Tillenius
Add an image:badges formatter for BugTaskListingItem.
2062
    def bug_badge_properties(self):
2063
        return getUtility(IBugTaskSet).getBugTaskBadgeProperties(
2064
            self.currentBatch())
2065
4334.1.6 by Gavin Panella
Create custom widget for review actions.
2066
    def _getListingItem(self, bugtask):
4310.1.16 by Bjorn Tillenius
add a bug icon to the +nominations listing.
2067
        """Return a decorated bugtask for the bug listing."""
6341.4.1 by Bjorn Tillenius
Add an image:badges formatter for BugTaskListingItem.
2068
        badge_property = self.bug_badge_properties[bugtask]
7675.568.4 by Tom Berger
Don't use target_context for calculating bug heat if the context is IPerson or IMaloneApplication - they don't have a max_bug_heat attribute.
2069
        if (IMaloneApplication.providedBy(self.target_context) or
2070
            IPerson.providedBy(self.target_context)):
2071
            # XXX Tom Berger bug=529846
2072
            # When we have a specific interface for things that have bug heat
2073
            # it would be better to use that for the check here instead.
2074
            target_context = None
2075
        else:
2076
            target_context = self.target_context
4334.1.6 by Gavin Panella
Create custom widget for review actions.
2077
        return BugTaskListingItem(
6341.4.2 by Bjorn Tillenius
remove unused bug_branches attribute.
2078
            bugtask,
6341.4.1 by Bjorn Tillenius
Add an image:badges formatter for BugTaskListingItem.
2079
            badge_property['has_branch'],
7675.480.1 by Tom Berger
display the bug heat flames in bug listings
2080
            badge_property['has_specification'],
7675.494.1 by Tom Berger
use the bugtask listing item decorator to get the information for the patch badge more efficiently.
2081
            badge_property['has_patch'],
7675.568.3 by Tom Berger
When creating the bug heat flames display, compare to the max heat of the current context, not the target of the bugtask.
2082
            request=self.request,
7675.568.4 by Tom Berger
Don't use target_context for calculating bug heat if the context is IPerson or IMaloneApplication - they don't have a max_bug_heat attribute.
2083
            target_context=target_context)
4334.1.6 by Gavin Panella
Create custom widget for review actions.
2084
3755.4.14 by Tim Penhey
following review comments
2085
    def getBugListingItems(self):
2086
        """Return a decorated list of visible bug tasks."""
4334.1.6 by Gavin Panella
Create custom widget for review actions.
2087
        return [self._getListingItem(bugtask) for bugtask in self.batch]
2088
2089
3864.2.14 by Tim Penhey
Fixing broken tests or ensuring use of quote or sqlvalues.
2090
class NominatedBugReviewAction(EnumeratedType):
4310.1.8 by Bjorn Tillenius
clean-up.
2091
    """Enumeration for nomination review actions"""
4334.1.6 by Gavin Panella
Create custom widget for review actions.
2092
3864.2.14 by Tim Penhey
Fixing broken tests or ensuring use of quote or sqlvalues.
2093
    ACCEPT = Item("""
4334.1.6 by Gavin Panella
Create custom widget for review actions.
2094
        Accept
2095
2096
        Accept the bug nomination.
2097
        """)
2098
3864.2.14 by Tim Penhey
Fixing broken tests or ensuring use of quote or sqlvalues.
2099
    DECLINE = Item("""
4334.1.6 by Gavin Panella
Create custom widget for review actions.
2100
        Decline
2101
2102
        Decline the bug nomination.
2103
        """)
2104
3864.2.14 by Tim Penhey
Fixing broken tests or ensuring use of quote or sqlvalues.
2105
    NO_CHANGE = Item("""
4334.1.6 by Gavin Panella
Create custom widget for review actions.
2106
        No change
2107
2108
        Do not change the status of the bug nomination.
2109
        """)
2110
2111
2112
class NominatedBugListingBatchNavigator(BugListingBatchNavigator):
4310.1.8 by Bjorn Tillenius
clean-up.
2113
    """Batch navigator for nominated bugtasks. """
2114
2115
    implements(INominationsReviewTableBatchNavigator)
2116
4310.1.5 by Bjorn Tillenius
only show the widgets if the user has access to approve the nomination.
2117
    def __init__(self, tasks, request, columns_to_show, size,
2118
                 nomination_target, user):
4974.2.1 by Curtis Hovey
Fixes per pylint.
2119
        BugListingBatchNavigator.__init__(
2120
            self, tasks, request, columns_to_show, size)
4334.1.7 by Gavin Panella
Finish widget and get updates working.
2121
        self.nomination_target = nomination_target
4310.1.5 by Bjorn Tillenius
only show the widgets if the user has access to approve the nomination.
2122
        self.user = user
4334.1.7 by Gavin Panella
Finish widget and get updates working.
2123
4334.1.6 by Gavin Panella
Create custom widget for review actions.
2124
    def _getListingItem(self, bugtask):
4310.1.14 by Bjorn Tillenius
add docstrings.
2125
        """See BugListingBatchNavigator."""
4310.1.5 by Bjorn Tillenius
only show the widgets if the user has access to approve the nomination.
2126
        bugtask_listing_item = BugListingBatchNavigator._getListingItem(
2127
            self, bugtask)
4310.1.4 by Bjorn Tillenius
clean-up
2128
        bug_nomination = bugtask_listing_item.bug.getNominationFor(
2129
            self.nomination_target)
4310.1.5 by Bjorn Tillenius
only show the widgets if the user has access to approve the nomination.
2130
        if self.user is None or not bug_nomination.canApprove(self.user):
2131
            return bugtask_listing_item
2132
4334.1.6 by Gavin Panella
Create custom widget for review actions.
2133
        review_action_field = Choice(
11626.3.10 by Curtis Hovey
Hush lints epic complaints about the changes files.
2134
            __name__='review_action_%d' % bug_nomination.id,
3864.2.14 by Tim Penhey
Fixing broken tests or ensuring use of quote or sqlvalues.
2135
            vocabulary=NominatedBugReviewAction,
4334.1.7 by Gavin Panella
Finish widget and get updates working.
2136
            title=u'Review action', required=True)
4334.1.6 by Gavin Panella
Create custom widget for review actions.
2137
2138
        # This is so setUpWidget expects a view, and so
2139
        # view.request. We're not passing a view but we still want it
2140
        # to work.
2141
        bugtask_listing_item.request = self.request
2142
4334.1.7 by Gavin Panella
Finish widget and get updates working.
2143
        bugtask_listing_item.review_action_widget = CustomWidgetFactory(
2144
            NominationReviewActionWidget)
4334.1.6 by Gavin Panella
Create custom widget for review actions.
2145
        setUpWidget(
11411.7.29 by j.c.sackett
Lint fixes.
2146
            bugtask_listing_item,
2147
            'review_action',
2148
            review_action_field,
2149
            IInputWidget,
2150
            value=NominatedBugReviewAction.NO_CHANGE,
2151
            context=bug_nomination)
4334.1.6 by Gavin Panella
Create custom widget for review actions.
2152
2153
        return bugtask_listing_item
3755.4.13 by Tim Penhey
Changed to custom batch navigator for bug listings
2154
2155
9315.2.1 by Abel Deuring
buglisting-default.pt converted to 3.0 layout
2156
class IBugTaskSearchListingMenu(Interface):
2157
    """A marker interface for the search listing navigation menu."""
2158
2159
2160
class BugTaskSearchListingMenu(NavigationMenu):
2161
    """The search listing navigation menu."""
2162
    usedfor = IBugTaskSearchListingMenu
2163
    facet = 'bugs'
2164
2165
    @property
2166
    def links(self):
2167
        bug_target = self.context.context
2168
        if IDistribution.providedBy(bug_target):
2169
            return (
2170
                'bugsupervisor',
2171
                'securitycontact',
2172
                'cve',
2173
                )
2174
        elif IDistroSeries.providedBy(bug_target):
2175
            return (
2176
                'cve',
2177
                'nominations',
2178
                )
2179
        elif IProduct.providedBy(bug_target):
2180
            return (
2181
                'bugsupervisor',
2182
                'securitycontact',
2183
                'cve',
2184
                )
2185
        elif IProductSeries.providedBy(bug_target):
2186
            return (
2187
                'nominations',
2188
                )
2189
        else:
2190
            return ()
2191
2192
    def cve(self):
2193
        return Link('+cve', 'CVE reports', icon='cve')
2194
2195
    @enabled_with_permission('launchpad.Edit')
2196
    def bugsupervisor(self):
2197
        return Link('+bugsupervisor', 'Change bug supervisor', icon='edit')
2198
2199
    @enabled_with_permission('launchpad.Edit')
2200
    def securitycontact(self):
9559.3.5 by Deryck Hodge
Clean lint.
2201
        return Link(
2202
            '+securitycontact', 'Change security contact', icon='edit')
9315.2.1 by Abel Deuring
buglisting-default.pt converted to 3.0 layout
2203
2204
    def nominations(self):
2205
        return Link('+nominations', 'Review nominations', icon='bug')
2206
2207
9570.15.4 by Gavin Panella
Split out the *_info properties into a mixin class.
2208
class BugTaskSearchListingView(LaunchpadFormView, FeedsMixin, BugsInfoMixin):
5155.3.9 by Jonathan Knowles
Improving the documentation comments for BugTaskSearchListingView and TextualBugTaskSearchListingView.
2209
    """View that renders a list of bugs for a given set of search criteria."""
3470.1.1 by Diogo Matsubara
Fix https://launchpad.net/products/malone/+bug/33978 (Advanced search page doesn't do any input validation)
2210
9315.2.1 by Abel Deuring
buglisting-default.pt converted to 3.0 layout
2211
    implements(IBugTaskSearchListingMenu)
2212
5429.1.2 by Edwin Grubbs
Changed templates and HasAnnouncementsView to use the FeedsMixin
2213
    # Only include <link> tags for bug feeds when using this view.
2214
    feed_types = (
2215
        BugTargetLatestBugsFeedLink,
2216
        )
2217
4584.2.7 by Graham Binns
Made Barry's requested changes
2218
    # These widgets are customised so as to keep the presentation of this view
2219
    # and its descendants consistent after refactoring to use
2220
    # LaunchpadFormView as a parent.
4584.2.1 by Graham Binns
Refactored BugTaskSearchListingView to inherit from LaunchpadFormView rather than LaunchpadView. Updated ZCML, templates and descendant classes accordingly.
2221
    custom_widget('searchtext', NewLineToSpacesWidget)
2222
    custom_widget('status_upstream', LabeledMultiCheckBoxWidget)
2223
    custom_widget('tag', BugTagsWidget)
5433.5.1 by Tom Berger
allow searching for bugs by tags either inclusively or exclusively
2224
    custom_widget('tags_combinator', RadioWidget)
4584.2.3 by Graham Binns
Trimmed unused code and comments from BugTaskSearchListingView
2225
    custom_widget('component', LabeledMultiCheckBoxWidget)
4584.2.1 by Graham Binns
Refactored BugTaskSearchListingView to inherit from LaunchpadFormView rather than LaunchpadView. Updated ZCML, templates and descendant classes accordingly.
2226
11664.3.3 by Edwin Grubbs
Don't display bugs or the Report-bug button if Launchpad doesn't know where it tracks bugs.
2227
    @cachedproperty
11664.3.1 by Edwin Grubbs
Disable Report-Bug button.
2228
    def bug_tracking_usage(self):
11655.1.12 by Brad Crittenden
Merged substantial changes to buglisting-default.pt
2229
        """Whether the context tracks bugs in Launchpad.
11664.3.1 by Edwin Grubbs
Disable Report-Bug button.
2230
2231
        :returns: ServiceUsage enum value
2232
        """
2233
        service_usage = IServiceUsage(self.context)
2234
        return service_usage.bug_tracking_usage
2235
11926.8.6 by Curtis Hovey
Updated view to provide state or object for template.
2236
    @cachedproperty
11926.8.2 by Curtis Hovey
Move the external_bugtracker property to the base view so that several templates
2237
    def external_bugtracker(self):
2238
        """External bug tracking system designated for the context.
2239
2240
        :returns: `IBugTracker` or None
2241
        """
2242
        has_external_bugtracker = IHasExternalBugTracker(self.context, None)
2243
        if has_external_bugtracker is None:
2244
            return None
2245
        else:
2246
            return has_external_bugtracker.getExternalBugTracker()
2247
2248
    @property
11926.8.6 by Curtis Hovey
Updated view to provide state or object for template.
2249
    def has_bugtracker(self):
11926.8.8 by Curtis Hovey
Added missing doctstrings.
2250
        """Does the `IBugTarget` have a bug tracker or use Launchpad?"""
11926.8.6 by Curtis Hovey
Updated view to provide state or object for template.
2251
        usage = IServiceUsage(self.context)
2252
        uses_lp = usage.bug_tracking_usage == ServiceUsage.LAUNCHPAD
2253
        if self.external_bugtracker or uses_lp:
2254
            return True
2255
        return False
2256
2257
    @property
2258
    def upstream_launchpad_project(self):
11926.8.8 by Curtis Hovey
Added missing doctstrings.
2259
        """The linked upstream `IProduct` for the package.
11926.8.6 by Curtis Hovey
Updated view to provide state or object for template.
2260
2261
        If this `IBugTarget` is a `IDistributionSourcePackage` or an
2262
        `ISourcePackage` and it is linked to an upstream project that uses
2263
        Launchpad to track bugs, return the `IProduct`. Otherwise,
2264
        return None
2265
2266
        :returns: `IProduct` or None
2267
        """
2268
        if self._sourcePackageContext():
2269
            sp = self.context
2270
        elif self._distroSourcePackageContext():
2271
            sp = self.context.development_version
2272
        else:
2273
            sp = None
2274
        if sp is not None:
2275
            packaging = sp.packaging
2276
            if packaging is not None:
2277
                product = packaging.productseries.product
2278
                if product.bug_tracking_usage == ServiceUsage.LAUNCHPAD:
2279
                    return product
2280
        return None
2281
2282
    @property
11655.1.6 by Brad Crittenden
add page heading and breadcrumbs to bugs page for project groups.
2283
    def page_title(self):
2284
        return "Bugs in %s" % self.context.title
2285
2286
    label = page_title
2287
2288
    @property
3811.2.3 by Bjorn Tillenius
initial go at making the search on the Bugs front page work.
2289
    def schema(self):
4974.2.1 by Curtis Hovey
Fixes per pylint.
2290
        """Return the schema that defines the form."""
3554.1.31 by Brad Bollenbach
checkpoint
2291
        if self._personContext():
3811.2.3 by Bjorn Tillenius
initial go at making the search on the Bugs front page work.
2292
            return IPersonBugTaskSearch
4712.3.3 by Curtis Hovey
Added link and search param for bugs needing forwarding upstream
2293
        elif self.isUpstreamProduct:
2294
            return IUpstreamProductBugTaskSearch
3554.1.31 by Brad Bollenbach
checkpoint
2295
        else:
3811.2.3 by Bjorn Tillenius
initial go at making the search on the Bugs front page work.
2296
            return IBugTaskSearch
3554.1.31 by Brad Bollenbach
checkpoint
2297
5309.1.3 by Edwin Grubbs
The <link> tag for referencing atom feeds in other web pages is now
2298
    @property
2299
    def feed_links(self):
2300
        """Prevent conflicts between the page and the atom feed.
2301
2302
        The latest-bugs atom feed matches the default output of this
2303
        view, but it does not match this view's bug listing when
2304
        any search parameters are passed in.
2305
        """
5309.1.10 by Edwin Grubbs
Fixed coding style issues
2306
        if self.request.get('QUERY_STRING', '') == '':
2307
            # There is no query in this request, so it's okay for this page to
2308
            # have its feed links.
2309
            return super(BugTaskSearchListingView, self).feed_links
2310
        else:
2311
            # The query changes the results so that they would not match the
2312
            # feed.  In this case, suppress the feed links.
5309.1.3 by Edwin Grubbs
The <link> tag for referencing atom feeds in other web pages is now
2313
            return []
2314
3811.2.3 by Bjorn Tillenius
initial go at making the search on the Bugs front page work.
2315
    def initialize(self):
4974.2.1 by Curtis Hovey
Fixes per pylint.
2316
        """Initialize the view with the request.
4974.2.2 by Curtis Hovey
Removed trailing whitespace.
2317
4974.2.1 by Curtis Hovey
Fixes per pylint.
2318
        Look for old status names and redirect to a new location if found.
2319
        """
4318.3.6 by Gavin Panella
Add redirection functionality.
2320
        query_string = self.request.get('QUERY_STRING')
2321
        if query_string:
4318.3.19 by Gavin Panella
Address bug-workflow-2 review comments from thumper.
2322
            query_string_rewritten = (
2323
                rewrite_old_bugtask_status_query_string(query_string))
4318.3.22 by Gavin Panella
Address additional bug-workflow-2 review comments from thumper.
2324
            if query_string_rewritten != query_string:
4318.3.19 by Gavin Panella
Address bug-workflow-2 review comments from thumper.
2325
                redirect_uri = URI(self.request.getURL()).replace(
2326
                    query=query_string_rewritten)
2327
                self.request.response.redirect(str(redirect_uri), status=301)
4318.3.6 by Gavin Panella
Add redirection functionality.
2328
                return
4318.3.19 by Gavin Panella
Address bug-workflow-2 review comments from thumper.
2329
4584.2.2 by Graham Binns
Updated bugtask-search-pages doctest to reflect the refactoring of BugTaskSearchListingView. Made a minor alteration to BugTaskSearchListingView.initialize() to prevent UnexpectedFormData errors.
2330
        self._migrateOldUpstreamStatus()
4584.2.1 by Graham Binns
Refactored BugTaskSearchListingView to inherit from LaunchpadFormView rather than LaunchpadView. Updated ZCML, templates and descendant classes accordingly.
2331
        LaunchpadFormView.initialize(self)
2332
2333
        # We call self._validate() here because LaunchpadFormView only
2334
        # validates the form if an action is submitted but, because this form
2335
        # can be called through a query string, we don't want to require an
4584.2.7 by Graham Binns
Made Barry's requested changes
2336
        # action. We pass an empty dict to _validate() because all the data
2337
        # needing validation is already available internally to self.
4584.2.1 by Graham Binns
Refactored BugTaskSearchListingView to inherit from LaunchpadFormView rather than LaunchpadView. Updated ZCML, templates and descendant classes accordingly.
2338
        self._validate(None, {})
2339
12393.30.7 by Brad Crittenden
Support new structural subscriptions for IProjectGroup
2340
        expose_structural_subscription_data_to_js(
2341
            self.context, self.request, self.user)
2342
3015.1.44 by Brad Bollenbach
checkpoint
2343
    @property
2344
    def columns_to_show(self):
2345
        """Returns a sequence of column names to be shown in the listing."""
2346
        upstream_context = self._upstreamContext()
3614.1.106 by Bjorn Tillenius
unbreak the productseries +bugs page.
2347
        productseries_context = self._productSeriesContext()
3216.2.1 by James Henstridge
commit project bug listing page patch
2348
        project_context = self._projectContext()
3015.1.44 by Brad Bollenbach
checkpoint
2349
        distribution_context = self._distributionContext()
4285.2.1 by Mark Shuttleworth
Massive renaming of distrorelease to distroseries
2350
        distroseries_context = self._distroSeriesContext()
3015.1.54 by Brad Bollenbach
response to code review
2351
        distrosourcepackage_context = self._distroSourcePackageContext()
2352
        sourcepackage_context = self._sourcePackageContext()
3015.1.44 by Brad Bollenbach
checkpoint
2353
3614.1.106 by Bjorn Tillenius
unbreak the productseries +bugs page.
2354
        if (upstream_context or productseries_context or
2355
            distrosourcepackage_context or sourcepackage_context):
7675.480.1 by Tom Berger
display the bug heat flames in bug listings
2356
            return ["id", "summary", "importance", "status", "heat"]
4285.2.1 by Mark Shuttleworth
Massive renaming of distrorelease to distroseries
2357
        elif distribution_context or distroseries_context:
7675.532.2 by Abel Deuring
upstream report template changed to show the number of bugs having patchs that might be intersting for upstream
2358
            return [
2359
                "id", "summary", "packagename", "importance", "status",
10230.1.3 by Deryck Hodge
Fix lint warnings.
2360
                "heat"]
3216.2.1 by James Henstridge
commit project bug listing page patch
2361
        elif project_context:
7675.532.2 by Abel Deuring
upstream report template changed to show the number of bugs having patchs that might be intersting for upstream
2362
            return [
2363
                "id", "summary", "productname", "importance", "status",
10230.1.3 by Deryck Hodge
Fix lint warnings.
2364
                "heat"]
3614.1.106 by Bjorn Tillenius
unbreak the productseries +bugs page.
2365
        else:
2366
            raise AssertionError(
2367
                "Unrecognized context; don't know which report "
2368
                "columns to show.")
3015.1.44 by Brad Bollenbach
checkpoint
2369
3554.1.31 by Brad Bollenbach
checkpoint
2370
    def validate_search_params(self):
2371
        """Validate the params passed for the search.
2372
2373
        An UnexpectedFormData exception is raised if the user submitted a URL
2374
        that could not have been created from the UI itself.
2375
        """
2376
        # The only way the user should get these field values incorrect is
2377
        # through a stale bookmark or a hand-hacked URL.
3554.1.62 by Brad Bollenbach
Fix bug 56700 (Advanced bug search crash when passing an invalid field.status_upstream value.)
2378
        for field_name in ("status", "importance", "milestone", "component",
2379
                           "status_upstream"):
5243.1.1 by Jonathan Knowles
Renaming method getWidgetError (in LaunchpadFormView) to getFieldError.
2380
            if self.getFieldError(field_name):
3554.1.31 by Brad Bollenbach
checkpoint
2381
                raise UnexpectedFormData(
2382
                    "Unexpected value for field '%s'. Perhaps your bookmarks "
4584.2.3 by Graham Binns
Trimmed unused code and comments from BugTaskSearchListingView
2383
                    "are out of date or you changed the URL by hand?" %
2384
                    field_name)
3691.158.4 by Bjorn Tillenius
fix bug 59972, don't oops when entering invalid tags on the advanced search.
2385
3554.1.31 by Brad Bollenbach
checkpoint
2386
        orderby = get_sortorder_from_request(self.request)
2387
        bugset = getUtility(IBugTaskSet)
2388
        for orderby_col in orderby:
2389
            if orderby_col.startswith("-"):
2390
                orderby_col = orderby_col[1:]
2391
2392
            try:
2393
                bugset.getOrderByColumnDBName(orderby_col)
2394
            except KeyError:
2395
                raise UnexpectedFormData(
2396
                    "Unknown sort column '%s'" % orderby_col)
2749 by Canonical.com Patch Queue Manager
r=salgado. first go at pre-defined bug listings. this merge also includes mpt's bug-listings-love, r=bjornt for that one.
2397
4785.2.1 by Tom Berger
customize the assignee input field so that it auto-selects its radio button on keypress; also add a pagetest for the advanced search form checking that the call to the JS function is there
2398
    def setUpWidgets(self):
2399
        """Customize the onKeyPress event of the assignee chooser."""
2400
        LaunchpadFormView.setUpWidgets(self)
2401
2402
        self.widgets["assignee"].onKeyPress = (
2403
            "selectWidget('assignee_option', event)")
2404
4584.2.1 by Graham Binns
Refactored BugTaskSearchListingView to inherit from LaunchpadFormView rather than LaunchpadView. Updated ZCML, templates and descendant classes accordingly.
2405
    def validate(self, data):
2406
        """Validates the form."""
2407
        self.validateVocabulariesAdvancedForm()
2408
        self.validate_search_params()
2409
4428.1.2 by Abel Deuring
bug 121550: implemented flacoste's (reviewers) suggestions
2410
    def _migrateOldUpstreamStatus(self):
4584.2.3 by Graham Binns
Trimmed unused code and comments from BugTaskSearchListingView
2411
        """Converts old upstream status value parameters to new ones.
2412
2413
        Before Launchpad version 1.1.6 (build 4412), the upstream parameter
2414
        in the request was a single string value, coming from a set of
4428.1.2 by Abel Deuring
bug 121550: implemented flacoste's (reviewers) suggestions
2415
        radio buttons. From that version on, the user can select multiple
2416
        values in the web UI. In order to keep old bookmarks working,
2417
        convert the old string parameter into a list.
2418
        """
2419
        old_upstream_status_values_to_new_values = {
2420
            'only_resolved_upstream': 'resolved_upstream'}
4584.2.5 by Graham Binns
Implemented bac's review requests
2421
4428.1.2 by Abel Deuring
bug 121550: implemented flacoste's (reviewers) suggestions
2422
        status_upstream = self.request.get('field.status_upstream')
2423
        if status_upstream in old_upstream_status_values_to_new_values.keys():
2424
            self.request.form['field.status_upstream'] = [
2425
                old_upstream_status_values_to_new_values[status_upstream]]
2426
        elif status_upstream == '':
2427
            del self.request.form['field.status_upstream']
2428
        else:
2429
            # The value of status_upstream is either correct, so nothing to
4584.2.3 by Graham Binns
Trimmed unused code and comments from BugTaskSearchListingView
2430
            # do, or it has some other error, which is handled in
2431
            # LaunchpadFormView's own validation.
4428.1.2 by Abel Deuring
bug 121550: implemented flacoste's (reviewers) suggestions
2432
            pass
2433
3691.326.17 by Guilherme Salgado
Fix a regression caught by a test
2434
    def buildSearchParams(self, searchtext=None, extra_params=None):
3691.326.13 by Guilherme Salgado
Fix https://beta.launchpad.net/launchpad/+bug/2982 (A person's Bugs page should show all bugs they are involved with)
2435
        """Build the BugTaskSearchParams object for the given arguments and
2436
        values specified by the user on this form's widgets.
2749 by Canonical.com Patch Queue Manager
r=salgado. first go at pre-defined bug listings. this merge also includes mpt's bug-listings-love, r=bjornt for that one.
2437
        """
4584.2.5 by Graham Binns
Implemented bac's review requests
2438
        # Calling _validate populates the data dictionary as a side-effect
2439
        # of validation.
4584.2.1 by Graham Binns
Refactored BugTaskSearchListingView to inherit from LaunchpadFormView rather than LaunchpadView. Updated ZCML, templates and descendant classes accordingly.
2440
        data = {}
2441
        self._validate(None, data)
3015.1.57 by Brad Bollenbach
make +bugs searches also work without the 'search' parameter
2442
2443
        if extra_params:
2444
            data.update(extra_params)
2445
2446
        if data:
3015.1.46 by Brad Bollenbach
checkpoint
2447
            searchtext = data.get("searchtext")
2448
            if searchtext and searchtext.isdigit():
2449
                try:
3015.1.47 by Brad Bollenbach
checkpoint
2450
                    bug = getUtility(IBugSet).get(searchtext)
3015.1.46 by Brad Bollenbach
checkpoint
2451
                except NotFoundError:
2452
                    pass
2453
                else:
2454
                    self.request.response.redirect(canonical_url(bug))
2455
3015.1.50 by Brad Bollenbach
add separate advanced search page
2456
            assignee_option = self.request.form.get("assignee_option")
2457
            if assignee_option == "none":
3015.1.46 by Brad Bollenbach
checkpoint
2458
                data['assignee'] = NULL
3015.1.57 by Brad Bollenbach
make +bugs searches also work without the 'search' parameter
2459
2460
            has_patch = data.pop("has_patch", False)
2461
            if has_patch:
4911.3.1 by Tom Berger
merge changes from rocketfuel and resolve conflicts
2462
                data["attachmenttype"] = BugAttachmentType.PATCH
3015.1.46 by Brad Bollenbach
checkpoint
2463
10326.4.2 by Abel Deuring
changed the layout of the 'advanced bug search' page (pacth provided by sinzui); changed the widgets for branch related search options.
2464
            has_branches = data.get('has_branches', True)
2465
            has_no_branches = data.get('has_no_branches', True)
2466
            if has_branches and not has_no_branches:
2467
                data['linked_branches'] = BugBranchSearch.BUGS_WITH_BRANCHES
2468
            elif not has_branches and has_no_branches:
2469
                data['linked_branches'] = (
2470
                    BugBranchSearch.BUGS_WITHOUT_BRANCHES)
2471
            else:
2472
                data['linked_branches'] = BugBranchSearch.ALL
2473
12278.3.1 by Gavin Panella
Untested UI for searching for bugs with or without blueprints.
2474
            has_blueprints = data.get('has_blueprints', True)
2475
            has_no_blueprints = data.get('has_no_blueprints', True)
2476
            if has_blueprints and not has_no_blueprints:
2477
                data['linked_blueprints'] = (
2478
                    BugBlueprintSearch.BUGS_WITH_BLUEPRINTS)
2479
            elif not has_blueprints and has_no_blueprints:
2480
                data['linked_blueprints'] = (
2481
                    BugBlueprintSearch.BUGS_WITHOUT_BLUEPRINTS)
2482
            else:
2483
                data['linked_blueprints'] = BugBlueprintSearch.ALL
2484
3367.2.1 by Brad Bollenbach
fix bug 35075 (Bug Triagers would benifit from a way to list bugs filed without a package)
2485
            # Filter appropriately if the user wants to restrict the
2486
            # search to only bugs with no package information.
2487
            has_no_package = data.pop("has_no_package", False)
2488
            if has_no_package:
2489
                data["sourcepackagename"] = NULL
2490
4317.5.2 by Abel Deuring
bug 6572: applied reviews comments
2491
        self._buildUpstreamStatusParams(data)
2492
2493
        # "Normalize" the form data into search arguments.
2494
        form_values = {}
2495
        for key, value in data.items():
5433.5.1 by Tom Berger
allow searching for bugs by tags either inclusively or exclusively
2496
            if key in ('tag'):
2497
                # Skip tag-related parameters, they
2498
                # are handled later on.
2499
                continue
4317.5.2 by Abel Deuring
bug 6572: applied reviews comments
2500
            if zope_isinstance(value, (list, tuple)):
2501
                if len(value) > 0:
2502
                    form_values[key] = any(*value)
2503
            else:
2504
                form_values[key] = value
2505
5433.5.1 by Tom Berger
allow searching for bugs by tags either inclusively or exclusively
2506
        if 'tag' in data:
2507
            # Tags require special handling, since they can be used
2508
            # to search either inclusively or exclusively.
2509
            # We take a look at the `tags_combinator` field, and wrap
5433.5.2 by Tom Berger
post review changes
2510
            # the tag list in the appropriate search directive (either
5433.5.1 by Tom Berger
allow searching for bugs by tags either inclusively or exclusively
2511
            # `any` or `all`). If no value is supplied, we assume `any`,
2512
            # in order to remain compatible with old saved search URLs.
2513
            tags = data['tag']
2514
            tags_combinator_all = (
5433.5.11 by Tom Berger
allow submitting the form without the combinator (so that old URLs will still work) and dont try to render the combinator widget if (like on a person) it isnt there
2515
                'tags_combinator' in data and
5433.5.1 by Tom Berger
allow searching for bugs by tags either inclusively or exclusively
2516
                data['tags_combinator'] == BugTagsSearchCombinator.ALL)
2517
            if zope_isinstance(tags, (list, tuple)) and len(tags) > 0:
2518
                if tags_combinator_all:
2519
                    form_values['tag'] = all(*tags)
2520
                else:
2521
                    form_values['tag'] = any(*tags)
2522
            else:
2523
                form_values['tag'] = tags
2524
9570.15.20 by Gavin Panella
Most callers of get_default_search_params() do not care about ordering.
2525
        search_params = get_default_search_params(self.user)
2526
        search_params.orderby = get_sortorder_from_request(self.request)
4317.5.2 by Abel Deuring
bug 6572: applied reviews comments
2527
        for name, value in form_values.items():
2528
            setattr(search_params, name, value)
2529
        return search_params
2530
2531
    def _buildUpstreamStatusParams(self, data):
2532
        """ Convert the status_upstream value to parameters we can
2533
        send to BugTaskSet.search().
2534
        """
3691.36.6 by Bjorn Tillenius
provide UI for showing only bugs that are closed elsewhere.
2535
        if 'status_upstream' in data:
2536
            status_upstream = data['status_upstream']
4317.5.1 by Abel Deuring
bug 6572: filter out bug with open upstream tasks in distribution bug searches
2537
            if 'pending_bugwatch' in status_upstream:
3691.36.6 by Bjorn Tillenius
provide UI for showing only bugs that are closed elsewhere.
2538
                data['pending_bugwatch_elsewhere'] = True
4317.5.1 by Abel Deuring
bug 6572: filter out bug with open upstream tasks in distribution bug searches
2539
            if 'resolved_upstream' in status_upstream:
2540
                data['resolved_upstream'] = True
2541
            if 'open_upstream' in status_upstream:
2542
                data['open_upstream'] = True
2543
            if 'hide_upstream' in status_upstream:
3553.3.44 by Brad Bollenbach
Fix the upstream status filter widget to 1. Ensure a radio button is selected by default, 2. Clarify the wording of each option, 3. Correct one filter option to work as intended for the known use cases
2544
                data['has_no_upstream_bugtask'] = True
3691.36.6 by Bjorn Tillenius
provide UI for showing only bugs that are closed elsewhere.
2545
            del data['status_upstream']
2546
4334.1.6 by Gavin Panella
Create custom widget for review actions.
2547
    def _getBatchNavigator(self, tasks):
4310.1.8 by Bjorn Tillenius
clean-up.
2548
        """Return the batch navigator to be used to batch the bugtasks."""
4334.1.6 by Gavin Panella
Create custom widget for review actions.
2549
        return BugListingBatchNavigator(
2550
            tasks, self.request, columns_to_show=self.columns_to_show,
7675.568.3 by Tom Berger
When creating the bug heat flames display, compare to the max heat of the current context, not the target of the bugtask.
2551
            size=config.malone.buglist_batch_size,
2552
            target_context=self.context)
4334.1.6 by Gavin Panella
Create custom widget for review actions.
2553
6666.2.4 by Tom Berger
a new interface method for searching bugtasks with named function params
2554
    def buildBugTaskSearchParams(self, searchtext=None, extra_params=None):
6666.2.22 by Tom Berger
search tasks using both the flat and deep interface with the same searchTasks method
2555
        """Build the parameters to submit to the `searchTasks` method.
6666.2.4 by Tom Berger
a new interface method for searching bugtasks with named function params
2556
2557
        Use the data submitted in the form to populate a dictionary
2558
        which, when expanded (using **params notation) can serve as the
6666.2.22 by Tom Berger
search tasks using both the flat and deep interface with the same searchTasks method
2559
        input for searchTasks().
6666.2.4 by Tom Berger
a new interface method for searching bugtasks with named function params
2560
        """
6666.2.37 by Tom Berger
fixes following brads review.
2561
2562
        # We force the view to populate the data dictionary by calling
2563
        # _validate here.
6666.2.4 by Tom Berger
a new interface method for searching bugtasks with named function params
2564
        data = {}
2565
        self._validate(None, data)
2566
6666.2.7 by Tom Berger
redirect to the bug page when the search text is a bug number ; also add a few more parameters that are being passed
2567
        searchtext = data.get("searchtext")
2568
        if searchtext and searchtext.isdigit():
2569
            try:
2570
                bug = getUtility(IBugSet).get(searchtext)
2571
            except NotFoundError:
2572
                pass
2573
            else:
2574
                self.request.response.redirect(canonical_url(bug))
2575
6666.2.4 by Tom Berger
a new interface method for searching bugtasks with named function params
2576
        if extra_params:
2577
            data.update(extra_params)
2578
2579
        params = {}
2580
6666.2.8 by Tom Berger
formatting and some comments
2581
        # A mapping of parameters that appear in the destination
6666.2.37 by Tom Berger
fixes following brads review.
2582
        # with a different name, or are being dropped altogether.
6666.2.8 by Tom Berger
formatting and some comments
2583
        param_names_map = {
2584
            'searchtext': 'search_text',
2585
            'omit_dupes': 'omit_duplicates',
2586
            'subscriber': 'bug_subscriber',
2587
            'tag': 'tags',
2588
            # The correct value is being retrieved
2589
            # using get_sortorder_from_request()
2590
            'orderby': None,
2591
            }
6666.2.4 by Tom Berger
a new interface method for searching bugtasks with named function params
2592
2593
        for key, value in data.items():
2594
            if key in param_names_map:
2595
                param_name = param_names_map[key]
2596
                if param_name is not None:
2597
                    params[param_name] = value
2598
            else:
2599
                params[key] = value
2600
2601
        assignee_option = self.request.form.get("assignee_option")
2602
        if assignee_option == "none":
2603
            params['assignee'] = NULL
2604
2605
        params['order_by'] = get_sortorder_from_request(self.request)
2606
2607
        return params
2608
3691.326.13 by Guilherme Salgado
Fix https://beta.launchpad.net/launchpad/+bug/2982 (A person's Bugs page should show all bugs they are involved with)
2609
    def search(self, searchtext=None, context=None, extra_params=None):
5155.3.9 by Jonathan Knowles
Improving the documentation comments for BugTaskSearchListingView and TextualBugTaskSearchListingView.
2610
        """Return an `ITableBatchNavigator` for the GET search criteria.
3691.326.13 by Guilherme Salgado
Fix https://beta.launchpad.net/launchpad/+bug/2982 (A person's Bugs page should show all bugs they are involved with)
2611
5020.3.10 by Curtis Hovey
Changes per review.
2612
        :param searchtext: Text that must occur in the bug report. If
2613
            searchtext is None, the search text will be gotten from the
2614
            request.
3691.326.13 by Guilherme Salgado
Fix https://beta.launchpad.net/launchpad/+bug/2982 (A person's Bugs page should show all bugs they are involved with)
2615
5020.3.9 by Curtis Hovey
Revisions per review.
2616
        :param extra_params: A dict that provides search params added to
2617
            the search criteria taken from the request. Params in
2618
            `extra_params` take precedence over request params.
3691.326.13 by Guilherme Salgado
Fix https://beta.launchpad.net/launchpad/+bug/2982 (A person's Bugs page should show all bugs they are involved with)
2619
        """
4755.1.46 by Curtis Hovey
Merge from RF. Resolved conflicts.
2620
        unbatchedTasks = self.searchUnbatched(
2621
            searchtext, context, extra_params)
5155.3.2 by Jonathan Knowles
Extracting method searchUnbatched(...) from method search(...) in class BugTaskSearchListingView.
2622
        return self._getBatchNavigator(unbatchedTasks)
2623
5020.3.10 by Curtis Hovey
Changes per review.
2624
    def searchUnbatched(self, searchtext=None, context=None,
11847.4.2 by Abel Deuring
new optional parameter prejoins for BugTaskSearchListingView.searchUnbatched() and for the same method of person related view classes; RelevantMilestonesMixin.getMilestoneWidgetValues() prejoins Milestone
2625
                        extra_params=None, prejoins=[]):
5155.3.9 by Jonathan Knowles
Improving the documentation comments for BugTaskSearchListingView and TextualBugTaskSearchListingView.
2626
        """Return a `SelectResults` object for the GET search criteria.
5155.3.2 by Jonathan Knowles
Extracting method searchUnbatched(...) from method search(...) in class BugTaskSearchListingView.
2627
5020.3.10 by Curtis Hovey
Changes per review.
2628
        :param searchtext: Text that must occur in the bug report. If
2629
            searchtext is None, the search text will be gotten from the
2630
            request.
5155.3.2 by Jonathan Knowles
Extracting method searchUnbatched(...) from method search(...) in class BugTaskSearchListingView.
2631
5020.3.9 by Curtis Hovey
Revisions per review.
2632
        :param extra_params: A dict that provides search params added to
2633
            the search criteria taken from the request. Params in
2634
            `extra_params` take precedence over request params.
5155.3.2 by Jonathan Knowles
Extracting method searchUnbatched(...) from method search(...) in class BugTaskSearchListingView.
2635
        """
3691.326.17 by Guilherme Salgado
Fix a regression caught by a test
2636
        # Base classes can provide an explicit search context.
2637
        if not context:
2638
            context = self.context
2639
6882.2.37 by Tom Berger
use the old search interface for compatibility with MaloneApplication
2640
        search_params = self.buildSearchParams(
3691.326.17 by Guilherme Salgado
Fix a regression caught by a test
2641
            searchtext=searchtext, extra_params=extra_params)
6882.2.37 by Tom Berger
use the old search interface for compatibility with MaloneApplication
2642
        search_params.user = self.user
11847.4.2 by Abel Deuring
new optional parameter prejoins for BugTaskSearchListingView.searchUnbatched() and for the same method of person related view classes; RelevantMilestonesMixin.getMilestoneWidgetValues() prejoins Milestone
2643
        tasks = context.searchTasks(search_params, prejoins=prejoins)
5155.3.2 by Jonathan Knowles
Extracting method searchUnbatched(...) from method search(...) in class BugTaskSearchListingView.
2644
        return tasks
1742 by Canonical.com Patch Queue Manager
fix critical bugs in distro side searching. huge code cleanup.
2645
4621.4.2 by Tom Berger
interim solution
2646
    def getWidgetValues(
2647
        self, vocabulary_name=None, vocabulary=None, default_values=()):
4621.4.8 by Tom Berger
post review changes
2648
        """Return data used to render a field's widget.
2649
2650
        Either `vocabulary_name` or `vocabulary` must be supplied."""
3015.1.50 by Brad Bollenbach
add separate advanced search page
2651
        widget_values = []
2652
4621.4.2 by Tom Berger
interim solution
2653
        if vocabulary is None:
2654
            assert vocabulary_name is not None, 'No vocabulary specified.'
2655
            vocabulary_registry = getVocabularyRegistry()
4974.2.1 by Curtis Hovey
Fixes per pylint.
2656
            vocabulary = vocabulary_registry.get(
2657
                self.context, vocabulary_name)
10326.4.3 by Abel Deuring
reverted a brain-dead code removal
2658
        for term in vocabulary:
2659
            widget_values.append(
2660
                dict(
2661
                    value=term.token, title=term.title or term.token,
2662
                    checked=term.value in default_values))
7675.718.1 by Abel Deuring
add a bug task status OPINION
2663
        return helpers.shortlist(widget_values, longest_expected=12)
3015.1.50 by Brad Bollenbach
add separate advanced search page
2664
2665
    def getStatusWidgetValues(self):
2666
        """Return data used to render the status checkboxes."""
2667
        return self.getWidgetValues(
4974.2.1 by Curtis Hovey
Fixes per pylint.
2668
            vocabulary=BugTaskStatusSearchDisplay,
10326.4.2 by Abel Deuring
changed the layout of the 'advanced bug search' page (pacth provided by sinzui); changed the widgets for branch related search options.
2669
            default_values=DEFAULT_SEARCH_BUGTASK_STATUSES_FOR_DISPLAY)
3015.1.50 by Brad Bollenbach
add separate advanced search page
2670
3270.3.10 by Matthew Paul Thomas
Changes BugTaskSeverity to BugTaskImportance.
2671
    def getImportanceWidgetValues(self):
2672
        """Return data used to render the Importance checkboxes."""
4911.3.1 by Tom Berger
merge changes from rocketfuel and resolve conflicts
2673
        return self.getWidgetValues(vocabulary=BugTaskImportance)
3015.1.50 by Brad Bollenbach
add separate advanced search page
2674
3249.3.1 by Brad Bollenbach
add milestone searching to the new advanced search form
2675
    def getMilestoneWidgetValues(self):
2676
        """Return data used to render the milestone checkboxes."""
2677
        return self.getWidgetValues("Milestone")
2678
3367.2.4 by Brad Bollenbach
fix bug 37911 (No advanced search for package bug reports)
2679
    def getSimpleSearchURL(self):
2680
        """Return a URL that can be used as an href to the simple search."""
3015.1.50 by Brad Bollenbach
add separate advanced search page
2681
        return canonical_url(self.context) + "/+bugs"
2682
2683
    def shouldShowAssigneeWidget(self):
2684
        """Should the assignee widget be shown on the advanced search page?"""
2685
        return True
2686
4678.4.2 by Jonathan Knowles
Adding new tests allowing page templates to check sanely whether certain fields are to be displayed.
2687
    def shouldShowCommenterWidget(self):
4974.2.1 by Curtis Hovey
Fixes per pylint.
2688
        """Show the commenter widget on the advanced search page?"""
4678.4.2 by Jonathan Knowles
Adding new tests allowing page templates to check sanely whether certain fields are to be displayed.
2689
        return True
2690
3283.2.4 by Brad Bollenbach
add a component option to the advanced search, for distro-aware contexts
2691
    def shouldShowComponentWidget(self):
4974.2.1 by Curtis Hovey
Fixes per pylint.
2692
        """Show the component widget on the advanced search page?"""
3283.2.4 by Brad Bollenbach
add a component option to the advanced search, for distro-aware contexts
2693
        context = self.context
2694
        return (
3485.6.5 by Brad Bollenbach
hide the component search widget when IDistribution.currentrelease is None
2695
            (IDistribution.providedBy(context) and
4285.2.1 by Mark Shuttleworth
Massive renaming of distrorelease to distroseries
2696
             context.currentseries is not None) or
2697
            IDistroSeries.providedBy(context) or
3485.6.5 by Brad Bollenbach
hide the component search widget when IDistribution.currentrelease is None
2698
            ISourcePackage.providedBy(context))
3283.2.4 by Brad Bollenbach
add a component option to the advanced search, for distro-aware contexts
2699
6025.3.26 by Tom Berger
typo
2700
    def shouldShowSupervisorWidget(self):
6087.7.2 by Tom Berger
convert bug contact to bug supervisor in template and browser code
2701
        """
2702
        Should the bug supervisor widget be shown on the advanced search page?
2703
        """
4678.4.2 by Jonathan Knowles
Adding new tests allowing page templates to check sanely whether certain fields are to be displayed.
2704
        return True
2705
3367.2.1 by Brad Bollenbach
fix bug 35075 (Bug Triagers would benifit from a way to list bugs filed without a package)
2706
    def shouldShowNoPackageWidget(self):
2707
        """Should the widget to filter on bugs with no package be shown?
2708
2709
        The widget will be shown only on a distribution or
4285.2.1 by Mark Shuttleworth
Massive renaming of distrorelease to distroseries
2710
        distroseries's advanced search page.
3367.2.1 by Brad Bollenbach
fix bug 35075 (Bug Triagers would benifit from a way to list bugs filed without a package)
2711
        """
2712
        return (IDistribution.providedBy(self.context) or
4285.2.1 by Mark Shuttleworth
Massive renaming of distrorelease to distroseries
2713
                IDistroSeries.providedBy(self.context))
3367.2.1 by Brad Bollenbach
fix bug 35075 (Bug Triagers would benifit from a way to list bugs filed without a package)
2714
3015.1.50 by Brad Bollenbach
add separate advanced search page
2715
    def shouldShowReporterWidget(self):
2716
        """Should the reporter widget be shown on the advanced search page?"""
2717
        return True
2718
5433.5.13 by Tom Berger
hide the tags_combinator widget wheere it doesnt belong; also merge changes from rocketfuel
2719
    def shouldShowTagsCombinatorWidget(self):
2720
        """Should the tags combinator widget show on the search page?"""
2721
        return True
2722
4285.2.5 by Mark Shuttleworth
Test fixes for renamed series
2723
    def shouldShowReleaseCriticalPortlet(self):
2724
        """Should the page include a portlet showing release-critical bugs
2725
        for different series.
2726
        """
2727
        return (
9760.8.1 by Brad Crittenden
Change the non-English 'serieses' to 'series' throughout our codebase.
2728
            IDistribution.providedBy(self.context) and self.context.series
4974.2.1 by Curtis Hovey
Fixes per pylint.
2729
            or IDistroSeries.providedBy(self.context)
9760.8.1 by Brad Crittenden
Change the non-English 'serieses' to 'series' throughout our codebase.
2730
            or IProduct.providedBy(self.context) and self.context.series
4974.2.1 by Curtis Hovey
Fixes per pylint.
2731
            or IProductSeries.providedBy(self.context))
4285.2.5 by Mark Shuttleworth
Test fixes for renamed series
2732
4678.4.2 by Jonathan Knowles
Adding new tests allowing page templates to check sanely whether certain fields are to be displayed.
2733
    def shouldShowSubscriberWidget(self):
4974.2.1 by Curtis Hovey
Fixes per pylint.
2734
        """Show the subscriber widget on the advanced search page?"""
4678.4.2 by Jonathan Knowles
Adding new tests allowing page templates to check sanely whether certain fields are to be displayed.
2735
        return True
2736
3553.3.44 by Brad Bollenbach
Fix the upstream status filter widget to 1. Ensure a radio button is selected by default, 2. Clarify the wording of each option, 3. Correct one filter option to work as intended for the known use cases
2737
    def shouldShowUpstreamStatusBox(self):
2738
        """Should the upstream status filtering widgets be shown?"""
4712.3.3 by Curtis Hovey
Added link and search param for bugs needing forwarding upstream
2739
        return self.isUpstreamProduct or not (
3553.3.44 by Brad Bollenbach
Fix the upstream status filter widget to 1. Ensure a radio button is selected by default, 2. Clarify the wording of each option, 3. Correct one filter option to work as intended for the known use cases
2740
            IProduct.providedBy(self.context) or
10326.1.1 by Henning Eggers
Mechanically renamed IProject* to IProjectGroup*.
2741
            IProjectGroup.providedBy(self.context))
3691.36.2 by Bjorn Tillenius
provide UI for searching for pending bug watches.
2742
1978 by Canonical.com Patch Queue Manager
[r=BjornT] search page love: better left-to-right column ordering,
2743
    def getSortLink(self, colname):
1716.1.139 by Christian Reis
Fix for bug 1358: Sort order should be indicated in Malone results. Refactors the sort handling code in browser/bugtask.py a bit as well.
2744
        """Return a link that can be used to sort results by colname."""
1978 by Canonical.com Patch Queue Manager
[r=BjornT] search page love: better left-to-right column ordering,
2745
        form = self.request.form
2746
        sortlink = ""
2747
        if form.get("search") is None:
2748
            # There is no search criteria to preserve.
2749
            sortlink = "%s?search=Search&orderby=%s" % (
2750
                str(self.request.URL), colname)
1716.1.139 by Christian Reis
Fix for bug 1358: Sort order should be indicated in Malone results. Refactors the sort handling code in browser/bugtask.py a bit as well.
2751
            return sortlink
2752
4664.1.1 by Curtis Hovey
Normalized comments for bug 3732.
2753
        # XXX: kiko 2005-08-23:
2754
        # Is it not possible to get the exact request supplied and
1716.1.139 by Christian Reis
Fix for bug 1358: Sort order should be indicated in Malone results. Refactors the sort handling code in browser/bugtask.py a bit as well.
2755
        # just sneak a "-" in front of the orderby argument, if it
2756
        # exists? If so, the code below could be a lot simpler.
2757
2758
        # There is search criteria to preserve.
2759
        sortlink = str(self.request.URL) + "?"
2760
        for fieldname in form:
2761
            fieldvalue = form.get(fieldname)
2762
            if isinstance(fieldvalue, (list, tuple)):
2763
                fieldvalue = [value.encode("utf-8") for value in fieldvalue]
2764
            else:
2765
                fieldvalue = fieldvalue.encode("utf-8")
2766
2767
            if fieldname != "orderby":
2768
                sortlink += "%s&" % urllib.urlencode(
11411.7.29 by j.c.sackett
Lint fixes.
2769
                    {fieldname: fieldvalue}, doseq=True)
1716.1.139 by Christian Reis
Fix for bug 1358: Sort order should be indicated in Malone results. Refactors the sort handling code in browser/bugtask.py a bit as well.
2770
2771
        sorted, ascending = self._getSortStatus(colname)
2772
        if sorted and ascending:
2773
            # If we are currently ascending, revert the direction
2774
            colname = "-" + colname
2775
2776
        sortlink += "orderby=%s" % colname
1978 by Canonical.com Patch Queue Manager
[r=BjornT] search page love: better left-to-right column ordering,
2777
2778
        return sortlink
2779
3554.1.35 by Brad Bollenbach
response to code review
2780
    def getSortedColumnCSSClass(self, colname):
3554.1.31 by Brad Bollenbach
checkpoint
2781
        """Return a class appropriate for sorted columns"""
2782
        sorted, ascending = self._getSortStatus(colname)
2783
        if not sorted:
2784
            return ""
2785
        if ascending:
2786
            return "sorted ascending"
2787
        return "sorted descending"
2788
2789
    def _getSortStatus(self, colname):
2790
        """Finds out if the list is sorted by the column specified.
2791
2792
        Returns a tuple (sorted, ascending), where sorted is true if the
2793
        list is currently sorted by the column specified, and ascending
2794
        is true if sorted in ascending order.
2795
        """
2796
        current_sort_column = self.request.form.get("orderby")
2797
        if current_sort_column is None:
2798
            return (False, False)
2799
2800
        ascending = True
2801
        sorted = True
2802
        if current_sort_column.startswith("-"):
2803
            ascending = False
2804
            current_sort_column = current_sort_column[1:]
2805
2806
        if current_sort_column != colname:
2807
            sorted = False
2808
2809
        return (sorted, ascending)
2810
2770.1.4 by Guilherme Salgado
Change all bug reports on people's page to use the google-style lists and have search/sort widgets. Also adds a new targetnamecache column in the BugTask table to allow sorting/searching on that value. Plus *a lot* other cleanup.
2811
    def shouldShowTargetName(self):
2812
        """Should the bug target name be displayed in the list of results?
2749 by Canonical.com Patch Queue Manager
r=salgado. first go at pre-defined bug listings. this merge also includes mpt's bug-listings-love, r=bjornt for that one.
2813
2814
        This is mainly useful for the listview.
2815
        """
2770.1.4 by Guilherme Salgado
Change all bug reports on people's page to use the google-style lists and have search/sort widgets. Also adds a new targetnamecache column in the BugTask table to allow sorting/searching on that value. Plus *a lot* other cleanup.
2816
        # It doesn't make sense to show the target name when viewing product
2817
        # bugs.
2818
        if IProduct.providedBy(self.context):
2819
            return False
2820
        else:
2607 by Canonical.com Patch Queue Manager
[r=SteveA] debongify the MSR integration on the D/DR bug listing,
2821
            return True
2822
3470.1.1 by Diogo Matsubara
Fix https://launchpad.net/products/malone/+bug/33978 (Advanced search page doesn't do any input validation)
2823
    def shouldShowAdvancedForm(self):
4974.2.1 by Curtis Hovey
Fixes per pylint.
2824
        """Return True if the advanced form should be shown, or False."""
3470.1.1 by Diogo Matsubara
Fix https://launchpad.net/products/malone/+bug/33978 (Advanced search page doesn't do any input validation)
2825
        if (self.request.form.get('advanced')
2826
            or self.form_has_errors):
2827
            return True
2828
        else:
2829
            return False
2830
3893.1.3 by Bjorn Tillenius
review comments.
2831
    @property
11655.1.1 by Brad Crittenden
Do not show bug information on a project group page with no projects that use Launchpad.
2832
    def should_show_bug_information(self):
11655.1.12 by Brad Crittenden
Merged substantial changes to buglisting-default.pt
2833
        return self.bug_tracking_usage == ServiceUsage.LAUNCHPAD
11655.1.1 by Brad Crittenden
Do not show bug information on a project group page with no projects that use Launchpad.
2834
2835
    @property
3893.1.3 by Bjorn Tillenius
review comments.
2836
    def form_has_errors(self):
11655.1.1 by Brad Crittenden
Do not show bug information on a project group page with no projects that use Launchpad.
2837
        """Return True if the form has errors, otherwise False."""
4584.2.1 by Graham Binns
Refactored BugTaskSearchListingView to inherit from LaunchpadFormView rather than LaunchpadView. Updated ZCML, templates and descendant classes accordingly.
2838
        return len(self.errors) > 0
3893.1.3 by Bjorn Tillenius
review comments.
2839
3470.1.1 by Diogo Matsubara
Fix https://launchpad.net/products/malone/+bug/33978 (Advanced search page doesn't do any input validation)
2840
    def validateVocabulariesAdvancedForm(self):
4584.2.3 by Graham Binns
Trimmed unused code and comments from BugTaskSearchListingView
2841
        """Provides a meaningful message for vocabulary validation errors."""
3470.1.1 by Diogo Matsubara
Fix https://launchpad.net/products/malone/+bug/33978 (Advanced search page doesn't do any input validation)
2842
        error_message = _(
4584.2.5 by Graham Binns
Implemented bac's review requests
2843
            "There's no person with the name or email address '%s'.")
4584.2.1 by Graham Binns
Refactored BugTaskSearchListingView to inherit from LaunchpadFormView rather than LaunchpadView. Updated ZCML, templates and descendant classes accordingly.
2844
6087.7.2 by Tom Berger
convert bug contact to bug supervisor in template and browser code
2845
        for name in ('assignee', 'bug_reporter', 'bug_supervisor',
4678.4.2 by Jonathan Knowles
Adding new tests allowing page templates to check sanely whether certain fields are to be displayed.
2846
                     'bug_commenter', 'subscriber'):
5243.1.1 by Jonathan Knowles
Renaming method getWidgetError (in LaunchpadFormView) to getFieldError.
2847
            if self.getFieldError(name):
4584.2.1 by Graham Binns
Refactored BugTaskSearchListingView to inherit from LaunchpadFormView rather than LaunchpadView. Updated ZCML, templates and descendant classes accordingly.
2848
                self.setFieldError(
2849
                    name, error_message %
5653.2.2 by Maris Fogels
Updated a whole whack of setFieldError() calls so that they use the new API.
2850
                        self.request.get('field.%s' % name))
3470.1.1 by Diogo Matsubara
Fix https://launchpad.net/products/malone/+bug/33978 (Advanced search page doesn't do any input validation)
2851
4712.3.3 by Curtis Hovey
Added link and search param for bugs needing forwarding upstream
2852
    @property
2853
    def isUpstreamProduct(self):
2854
        """Is the context a Product that does not use Malone?"""
2855
        return (
2856
            IProduct.providedBy(self.context)
11411.7.1 by j.c.sackett
Fixed majority of official_malone calls in code-space. Still need to fix templates.
2857
            and self.context.bug_tracking_usage != ServiceUsage.LAUNCHPAD)
4712.3.3 by Curtis Hovey
Added link and search param for bugs needing forwarding upstream
2858
1895 by Canonical.com Patch Queue Manager
fix clicking Advanced on distro page,
2859
    def _upstreamContext(self):
2860
        """Is this page being viewed in an upstream context?
2861
2862
        Return the IProduct if yes, otherwise return None.
2863
        """
2179 by Canonical.com Patch Queue Manager
[r=SteveA] remove IBugTaskSubset
2864
        return IProduct(self.context, None)
1895 by Canonical.com Patch Queue Manager
fix clicking Advanced on distro page,
2865
3614.1.106 by Bjorn Tillenius
unbreak the productseries +bugs page.
2866
    def _productSeriesContext(self):
2867
        """Is this page being viewed in a product series context?
2868
2869
        Return the IProductSeries if yes, otherwise return None.
2870
        """
2871
        return IProductSeries(self.context, None)
2872
3216.2.1 by James Henstridge
commit project bug listing page patch
2873
    def _projectContext(self):
2874
        """Is this page being viewed in a project context?
2875
10326.1.1 by Henning Eggers
Mechanically renamed IProject* to IProjectGroup*.
2876
        Return the IProjectGroup if yes, otherwise return None.
3216.2.1 by James Henstridge
commit project bug listing page patch
2877
        """
10326.1.1 by Henning Eggers
Mechanically renamed IProject* to IProjectGroup*.
2878
        return IProjectGroup(self.context, None)
3216.2.1 by James Henstridge
commit project bug listing page patch
2879
2770.1.4 by Guilherme Salgado
Change all bug reports on people's page to use the google-style lists and have search/sort widgets. Also adds a new targetnamecache column in the BugTask table to allow sorting/searching on that value. Plus *a lot* other cleanup.
2880
    def _personContext(self):
2881
        """Is this page being viewed in a person context?
2882
2883
        Return the IPerson if yes, otherwise return None.
2884
        """
2885
        return IPerson(self.context, None)
2886
1895 by Canonical.com Patch Queue Manager
fix clicking Advanced on distro page,
2887
    def _distributionContext(self):
2888
        """Is this page being viewed in a distribution context?
2889
2890
        Return the IDistribution if yes, otherwise return None.
2891
        """
2179 by Canonical.com Patch Queue Manager
[r=SteveA] remove IBugTaskSubset
2892
        return IDistribution(self.context, None)
1895 by Canonical.com Patch Queue Manager
fix clicking Advanced on distro page,
2893
4285.2.1 by Mark Shuttleworth
Massive renaming of distrorelease to distroseries
2894
    def _distroSeriesContext(self):
2895
        """Is this page being viewed in a distroseries context?
1895 by Canonical.com Patch Queue Manager
fix clicking Advanced on distro page,
2896
4285.2.1 by Mark Shuttleworth
Massive renaming of distrorelease to distroseries
2897
        Return the IDistroSeries if yes, otherwise return None.
1895 by Canonical.com Patch Queue Manager
fix clicking Advanced on distro page,
2898
        """
4285.2.1 by Mark Shuttleworth
Massive renaming of distrorelease to distroseries
2899
        return IDistroSeries(self.context, None)
2179 by Canonical.com Patch Queue Manager
[r=SteveA] remove IBugTaskSubset
2900
1716.3.20 by kiko
Fix for bug 6697: Source package bugs list is missing filter links. Makes the filter links portlet (on the RHS) available in distro source package and distrorelease source package pages; also cleans up lint and removes some fluff (hopefully not crashing some crackheaded tests, but..)
2901
    def _sourcePackageContext(self):
4974.2.1 by Curtis Hovey
Fixes per pylint.
2902
        """Is this view in a [distroseries] sourcepackage context?
1716.3.20 by kiko
Fix for bug 6697: Source package bugs list is missing filter links. Makes the filter links portlet (on the RHS) available in distro source package and distrorelease source package pages; also cleans up lint and removes some fluff (hopefully not crashing some crackheaded tests, but..)
2903
2904
        Return the ISourcePackage if yes, otherwise return None.
2905
        """
2906
        return ISourcePackage(self.context, None)
2907
2908
    def _distroSourcePackageContext(self):
2909
        """Is this page being viewed in a distribution sourcepackage context?
2910
2911
        Return the IDistributionSourcePackage if yes, otherwise return None.
2912
        """
2913
        return IDistributionSourcePackage(self.context, None)
1978 by Canonical.com Patch Queue Manager
[r=BjornT] search page love: better left-to-right column ordering,
2914
9389.10.9 by Tom Berger
interim commit
2915
    @property
9389.10.20 by Tom Berger
more test fixes
2916
    def addquestion_url(self):
2917
        """Return the URL for the +addquestion view for the context."""
2918
        if IQuestionTarget.providedBy(self.context):
2919
            return canonical_url(
2920
                self.context, rootsite='answers', view_name='+addquestion')
2921
        else:
2922
            return None
2923
9389.10.9 by Tom Berger
interim commit
2924
4334.1.2 by Gavin Panella
Add bug nominations page for distroseries.
2925
class BugNominationsView(BugTaskSearchListingView):
2926
    """View for accepting/declining bug nominations."""
2927
4334.1.6 by Gavin Panella
Create custom widget for review actions.
2928
    def _getBatchNavigator(self, tasks):
4310.1.8 by Bjorn Tillenius
clean-up.
2929
        """See BugTaskSearchListingView."""
4334.1.6 by Gavin Panella
Create custom widget for review actions.
2930
        batch_navigator = NominatedBugListingBatchNavigator(
2931
            tasks, self.request, columns_to_show=self.columns_to_show,
4310.1.5 by Bjorn Tillenius
only show the widgets if the user has access to approve the nomination.
2932
            size=config.malone.buglist_batch_size,
2933
            nomination_target=self.context, user=self.user)
4334.1.6 by Gavin Panella
Create custom widget for review actions.
2934
        return batch_navigator
2935
4334.1.2 by Gavin Panella
Add bug nominations page for distroseries.
2936
    def search(self):
4334.1.5 by Gavin Panella
Use a custom template for nominated bugs by using a marker interface.
2937
        """Return all the nominated tasks for this series."""
4310.1.21 by Bjorn Tillenius
add a +nominations page for a product series.
2938
        if IDistroSeries.providedBy(self.context):
2939
            main_context = self.context.distribution
2940
        elif IProductSeries.providedBy(self.context):
2941
            main_context = self.context.product
2942
        else:
2943
            raise AssertionError(
2944
                'Unknown nomination target: %r' % self.context)
4334.1.6 by Gavin Panella
Create custom widget for review actions.
2945
        return BugTaskSearchListingView.search(
4310.1.21 by Bjorn Tillenius
add a +nominations page for a product series.
2946
            self, context=main_context,
4334.1.2 by Gavin Panella
Add bug nominations page for distroseries.
2947
            extra_params=dict(nominated_for=self.context))
2948
2949
4334.1.7 by Gavin Panella
Finish widget and get updates working.
2950
class NominationsReviewTableBatchNavigatorView(LaunchpadFormView):
4310.1.7 by Bjorn Tillenius
clean-up
2951
    """View for displaying a list of nominated bugs."""
2952
4310.1.11 by Bjorn Tillenius
don't show the button unless the user actually can approve any of the nominations.
2953
    def canApproveNominations(self, action=None):
2954
        """Whether the user can approve any of the shown nominations."""
2955
        return len(list(self.widgets)) > 0
2956
4334.1.7 by Gavin Panella
Finish widget and get updates working.
2957
    def setUpFields(self):
4310.1.7 by Bjorn Tillenius
clean-up
2958
        """See LaunchpadFormView."""
2959
        # We set up the widgets ourselves.
4334.1.7 by Gavin Panella
Finish widget and get updates working.
2960
        self.form_fields = []
2961
2962
    def setUpWidgets(self):
4310.1.7 by Bjorn Tillenius
clean-up
2963
        """See LaunchpadFormView."""
4310.1.5 by Bjorn Tillenius
only show the widgets if the user has access to approve the nomination.
2964
        widgets_list = [
2965
            (True, bug_listing_item.review_action_widget)
2966
            for bug_listing_item in self.context.getBugListingItems()
2967
            if bug_listing_item.review_action_widget is not None]
13023.3.2 by William Grant
Lint.
2968
        self.widgets = formlib.form.Widgets(
2969
            widgets_list, len(self.prefix) + 1)
4334.1.7 by Gavin Panella
Finish widget and get updates working.
2970
10230.1.3 by Deryck Hodge
Fix lint warnings.
2971
    @action('Save changes', name='submit', condition=canApproveNominations)
4334.1.7 by Gavin Panella
Finish widget and get updates working.
2972
    def submit_action(self, action, data):
4310.1.7 by Bjorn Tillenius
clean-up
2973
        """Accept/Decline bug nominations."""
4334.1.7 by Gavin Panella
Finish widget and get updates working.
2974
        accepted = declined = 0
2975
2976
        for name, review_action in data.items():
2977
            if review_action == NominatedBugReviewAction.NO_CHANGE:
2978
                continue
2979
            field = self.widgets[name].context
2980
            bug_nomination = field.context
2981
            if review_action == NominatedBugReviewAction.ACCEPT:
2982
                bug_nomination.approve(self.user)
2983
                accepted += 1
2984
            elif review_action == NominatedBugReviewAction.DECLINE:
2985
                bug_nomination.decline(self.user)
2986
                declined += 1
2987
            else:
2988
                raise AssertionError(
11411.7.29 by j.c.sackett
Lint fixes.
2989
                    'Unknown NominatedBugReviewAction: %r' % review_action)
4334.1.7 by Gavin Panella
Finish widget and get updates working.
2990
2991
        if accepted > 0:
2992
            self.request.response.addInfoNotification(
2993
                '%d nomination(s) accepted' % accepted)
2994
        if declined > 0:
2995
            self.request.response.addInfoNotification(
2996
                '%d nomination(s) declined' % declined)
2997
2998
        self.next_url = self.request.getURL()
2999
        query_string = self.request.get('QUERY_STRING')
3000
        if query_string:
3001
            self.next_url += '?%s' % query_string
3002
3003
3691.285.1 by Bjorn Tillenius
add a most recently updated bugs portlet, including tests for distributions and products.
3004
class BugTargetView(LaunchpadView):
2268 by Canonical.com Patch Queue Manager
[r=bjornt] Refactor IBugTarget.searchBugs method to accept a BugTaskSearchParams instance, which avoids the need to duplicate the argument list in all targets. All callsites are now updated -- and a number of little privacy bugs fixed in tandem. Also renamed searchBugs to searchTasks, fixed some lintage and smashed it all in :-)
3005
    """Used to grab bugs for a bug target; used by the latest bugs portlet"""
3691.285.1 by Bjorn Tillenius
add a most recently updated bugs portlet, including tests for distributions and products.
3006
2268 by Canonical.com Patch Queue Manager
[r=bjornt] Refactor IBugTarget.searchBugs method to accept a BugTaskSearchParams instance, which avoids the need to duplicate the argument list in all targets. All callsites are now updated -- and a number of little privacy bugs fixed in tandem. Also renamed searchBugs to searchTasks, fixed some lintage and smashed it all in :-)
3007
    def latestBugTasks(self, quantity=5):
3008
        """Return <quantity> latest bugs reported against this target."""
2282 by Canonical.com Patch Queue Manager
[r=spiv] MaloneSourcePackageBugListing implementation
3009
        params = BugTaskSearchParams(orderby="-datecreated",
1716.5.45 by kiko
Fix for bug #5834: "Latest bugs in X" portlet should hide duplicates. Omit dupes in the portlet.
3010
                                     omit_dupes=True,
2268 by Canonical.com Patch Queue Manager
[r=bjornt] Refactor IBugTarget.searchBugs method to accept a BugTaskSearchParams instance, which avoids the need to duplicate the argument list in all targets. All callsites are now updated -- and a number of little privacy bugs fixed in tandem. Also renamed searchBugs to searchTasks, fixed some lintage and smashed it all in :-)
3011
                                     user=getUtility(ILaunchBag).user)
3012
3013
        tasklist = self.context.searchTasks(params)
3014
        return tasklist[:quantity]
3015
3691.285.1 by Bjorn Tillenius
add a most recently updated bugs portlet, including tests for distributions and products.
3016
    def getMostRecentlyUpdatedBugTasks(self, limit=5):
3017
        """Return the most recently updated bugtasks for this target."""
3018
        params = BugTaskSearchParams(
3019
            orderby="-date_last_updated", omit_dupes=True, user=self.user)
5821.9.2 by Bjorn Tillenius
return a list instead of a SelectsResults. makes bugtarget-recently-touched-bugs.txt pass.
3020
        return list(self.context.searchTasks(params)[:limit])
3691.285.1 by Bjorn Tillenius
add a most recently updated bugs portlet, including tests for distributions and products.
3021
3022
5155.3.3 by Jonathan Knowles
Changing TextualBugTaskSearchListingView to inherit directly from BugTaskSearchListingView.
3023
class TextualBugTaskSearchListingView(BugTaskSearchListingView):
4755.1.46 by Curtis Hovey
Merge from RF. Resolved conflicts.
3024
    """View that renders a list of bug IDs for a given set of search criteria.
3025
    """
3049.2.3 by Dafydd Harries
feedback from Steve
3026
3049.2.4 by Dafydd Harries
feedback from Steve
3027
    def render(self):
4974.2.1 by Curtis Hovey
Fixes per pylint.
3028
        """Render the BugTarget for text display."""
6152.1.1 by Francis J. Lacoste
Use raw SQL for TextualBugTaskSearchLisingView since this is unbatched.
3029
        self.request.response.setHeader(
3030
            'Content-type', 'text/plain')
3031
3032
        # This uses the BugTaskSet internal API instead of using the
6666.2.37 by Tom Berger
fixes following brads review.
3033
        # standard searchTasks() because the latter can retrieve a lot
3034
        # of bugs and we don't want to load all of that data in memory.
6152.1.1 by Francis J. Lacoste
Use raw SQL for TextualBugTaskSearchLisingView since this is unbatched.
3035
        # Retrieving only the bug numbers is much more efficient.
3036
        search_params = self.buildSearchParams()
3037
6767.8.30 by Maris Fogels
Added an XXX.
3038
        # XXX flacoste 2008/04/24 This should be moved to a
6152.1.1 by Francis J. Lacoste
Use raw SQL for TextualBugTaskSearchLisingView since this is unbatched.
3039
        # BugTaskSearchParams.setTarget().
12415.4.1 by Robert Collins
Also do milestone bug listings with one pass over the bugtasks table. Drive by to handle conjoined bugtasks properly if someone needs that in countBugs.
3040
        if (IDistroSeries.providedBy(self.context) or
3041
            IProductSeries.providedBy(self.context)):
3042
            search_params.setTarget(self.context)
6152.1.1 by Francis J. Lacoste
Use raw SQL for TextualBugTaskSearchLisingView since this is unbatched.
3043
        elif IDistribution.providedBy(self.context):
3044
            search_params.setDistribution(self.context)
3045
        elif IProduct.providedBy(self.context):
3046
            search_params.setProduct(self.context)
10326.1.1 by Henning Eggers
Mechanically renamed IProject* to IProjectGroup*.
3047
        elif IProjectGroup.providedBy(self.context):
6152.1.1 by Francis J. Lacoste
Use raw SQL for TextualBugTaskSearchLisingView since this is unbatched.
3048
            search_params.setProject(self.context)
3049
        elif (ISourcePackage.providedBy(self.context) or
3050
              IDistributionSourcePackage.providedBy(self.context)):
3051
            search_params.setSourcePackage(self.context)
3052
        else:
3053
            raise AssertionError('Uknown context type: %s' % self.context)
3054
11411.7.29 by j.c.sackett
Lint fixes.
3055
        return u"".join("%d\n" % bug_id for bug_id in
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
3056
            getUtility(IBugTaskSet).searchBugIds(search_params))
3049.2.1 by Dafydd Harries
add bug text pages
3057
3614.1.68 by Brad Bollenbach
reapply MaloneReleaseManagement
3058
3059
def _by_targetname(bugtask):
3060
    """Normalize the bugtask.targetname, for sorting."""
4002.7.36 by Matthew Paul Thomas
Makes bug pages work again.
3061
    return re.sub(r"\W", "", bugtask.bugtargetdisplayname)
3614.1.68 by Brad Bollenbach
reapply MaloneReleaseManagement
3062
6291.1.6 by Bjorn Tillenius
cache the milestone vocabularies.
3063
3064
class CachedMilestoneSourceFactory:
3065
    """A factory for milestone vocabularies.
3066
3067
    When rendering a page with many bug tasks, this factory is useful,
3068
    in order to avoid the number of db queries issues. For each bug task
3069
    target, we cache the milestone vocabulary, so we don't have to
3070
    create a new one for each target.
3071
    """
3072
3073
    implements(IContextSourceBinder)
3074
3075
    def __init__(self):
3076
        self.vocabularies = {}
12534.1.2 by Robert Collins
Reuse the milestone cache in bugtasks for the bugtask rows rather than just the bugtask edit forms.
3077
        self.contexts = set()
6291.1.6 by Bjorn Tillenius
cache the milestone vocabularies.
3078
3079
    def __call__(self, context):
12534.1.2 by Robert Collins
Reuse the milestone cache in bugtasks for the bugtask rows rather than just the bugtask edit forms.
3080
        assert context in self.contexts, ("context %r not added to "
3081
            "self.contexts (%r)." % (context, self.contexts))
3082
        self._load()
6291.1.6 by Bjorn Tillenius
cache the milestone vocabularies.
3083
        target = MilestoneVocabulary.getMilestoneTarget(context)
12534.1.2 by Robert Collins
Reuse the milestone cache in bugtasks for the bugtask rows rather than just the bugtask edit forms.
3084
        return self.vocabularies[target]
3085
3086
    def _load(self):
12534.1.4 by Robert Collins
Actually load the vocabularies just once.
3087
        """Load all the vocabularies, once only."""
3088
        if self.vocabularies:
3089
            return
12534.1.2 by Robert Collins
Reuse the milestone cache in bugtasks for the bugtask rows rather than just the bugtask edit forms.
3090
        targets = set(
3091
            map(MilestoneVocabulary.getMilestoneTarget, self.contexts))
3092
        # TODO: instantiate for all targets at once.
3093
        for target in targets:
3094
            milestone_vocabulary = MilestoneVocabulary(target)
6291.1.6 by Bjorn Tillenius
cache the milestone vocabularies.
3095
            self.vocabularies[target] = milestone_vocabulary
3096
3097
3614.1.68 by Brad Bollenbach
reapply MaloneReleaseManagement
3098
class BugTasksAndNominationsView(LaunchpadView):
3099
    """Browser class for rendering the bugtasks and nominations table."""
3100
6665.4.2 by Bjorn Tillenius
first attempt at making the target links more efficient.
3101
    target_releases = None
3102
3614.1.68 by Brad Bollenbach
reapply MaloneReleaseManagement
3103
    def __init__(self, context, request):
3104
        """Ensure we always have a bug context."""
3105
        LaunchpadView.__init__(self, IBug(context), request)
6665.4.10 by Bjorn Tillenius
review comments.
3106
3107
    def initialize(self):
3108
        """Cache the list of bugtasks and set up the release mapping."""
6291.1.8 by Bjorn Tillenius
cache bug.isSubscribed(useelf.ser)
3109
        # Cache some values, so that we don't have to recalculate them
12799.1.2 by Ian Booth
Rework implementation and do unit tests instead of doc tests
3110
        # for each bug task.
3111
        # Note: even though the publisher queries all the bugtasks and we in
3112
        # theory could just reuse that already loaded list here, it's better
3113
        # to do another query to only load the bug tasks for active projects
3114
        # so we don't incur the cost of setting up data structures for tasks
3115
        # we will not be showing in the listing.
3116
        bugtask_set = getUtility(IBugTaskSet)
3117
        search_params = BugTaskSearchParams(user=self.user, bug=self.context)
3118
        self.bugtasks = list(bugtask_set.search(search_params))
9521.3.1 by Tom Berger
patch from allenap
3119
        self.many_bugtasks = len(self.bugtasks) >= 10
6291.1.6 by Bjorn Tillenius
cache the milestone vocabularies.
3120
        self.cached_milestone_source = CachedMilestoneSourceFactory()
6291.1.8 by Bjorn Tillenius
cache bug.isSubscribed(useelf.ser)
3121
        self.user_is_subscribed = self.context.isSubscribed(self.user)
12599.2.1 by j.c.sackett
Eager loading apparatus in place.
3122
12799.1.2 by Ian Booth
Rework implementation and do unit tests instead of doc tests
3123
        # Pull all of the related milestones, if any, into the storm cache,
3124
        # since they'll be needed for the vocabulary used in this view.
12799.1.3 by Ian Booth
Fix stylistic issue
3125
        if self.bugtasks:
12799.1.2 by Ian Booth
Rework implementation and do unit tests instead of doc tests
3126
            self.milestones = list(
3127
                bugtask_set.getBugTaskTargetMilestones(self.bugtasks))
12799.1.3 by Ian Booth
Fix stylistic issue
3128
        else:
12929.7.46 by j.c.sackett
Lint fixes.
3129
            self.milestones = []
12412.1.1 by Robert Collins
Refactor lookups of target releases to do one getCurrentSourceReleases for all distros and one for all distroseries.
3130
        distro_packages = defaultdict(list)
3131
        distro_series_packages = defaultdict(list)
6665.4.5 by Bjorn Tillenius
cache the list of bug tasks.
3132
        for bugtask in self.bugtasks:
6665.4.2 by Bjorn Tillenius
first attempt at making the target links more efficient.
3133
            target = bugtask.target
3134
            if IDistributionSourcePackage.providedBy(target):
6665.4.8 by Bjorn Tillenius
fix list warnings.
3135
                distro_packages[target.distribution].append(
3136
                    target.sourcepackagename)
6665.4.2 by Bjorn Tillenius
first attempt at making the target links more efficient.
3137
            if ISourcePackage.providedBy(target):
12412.1.1 by Robert Collins
Refactor lookups of target releases to do one getCurrentSourceReleases for all distros and one for all distroseries.
3138
                distro_series_packages[target.distroseries].append(
6665.4.8 by Bjorn Tillenius
fix list warnings.
3139
                    target.sourcepackagename)
12412.1.1 by Robert Collins
Refactor lookups of target releases to do one getCurrentSourceReleases for all distros and one for all distroseries.
3140
        distro_set = getUtility(IDistributionSet)
3141
        self.target_releases = dict(distro_set.getCurrentSourceReleases(
3142
            distro_packages))
3143
        distro_series_set = getUtility(IDistroSeriesSet)
3144
        self.target_releases.update(
12622.5.3 by Curtis Hovey
Hushed lint.
3145
            distro_series_set.getCurrentSourceReleases(
3146
                distro_series_packages))
12517.1.1 by Robert Collins
Eager load the people needed to do package release tooltips on BugTask:+index
3147
        ids = set()
3148
        for release_person_ids in map(attrgetter('creatorID', 'maintainerID'),
3149
            self.target_releases.values()):
3150
            ids.update(release_person_ids)
3151
        ids.discard(None)
3152
        if ids:
3153
            list(getUtility(IPersonSet).getPrecachedPersonsFromIDs(ids))
6665.4.2 by Bjorn Tillenius
first attempt at making the target links more efficient.
3154
6665.4.1 by Bjorn Tillenius
first naive implementation.
3155
    def getTargetLinkTitle(self, target):
3156
        """Return text to put as the title for the link to the target."""
3157
        if not (IDistributionSourcePackage.providedBy(target) or
3158
                ISourcePackage.providedBy(target)):
3159
            return None
6665.4.2 by Bjorn Tillenius
first attempt at making the target links more efficient.
3160
        current_release = self.target_releases.get(target)
6665.4.1 by Bjorn Tillenius
first naive implementation.
3161
        if current_release is None:
3162
            return "No current release for this source package in %s" % (
3163
                target.distribution.displayname)
3164
        uploader = current_release.creator
3165
        maintainer = current_release.maintainer
3166
        return (
3167
            "Latest release: %(version)s, uploaded to %(component)s"
3168
            " on %(date_uploaded)s by %(uploader)s,"
3169
            " maintained by %(maintainer)s" % dict(
3170
                version=current_release.version,
3171
                component=current_release.component.name,
3172
                date_uploaded=current_release.dateuploaded,
3173
                uploader=uploader.unique_displayname,
3174
                maintainer=maintainer.unique_displayname,
3175
                ))
3176
6291.1.3 by Bjorn Tillenius
get rid of all the repeated Question queries on the bug page.
3177
    def _getTableRowView(self, context, is_converted_to_question,
3178
                         is_conjoined_slave):
3179
        """Get the view for the context, and initialize it.
3180
3181
        The view's is_conjoined_slave and is_converted_to_question
6291.1.6 by Bjorn Tillenius
cache the milestone vocabularies.
3182
        attributes are set, as well as the edit view.
6291.1.3 by Bjorn Tillenius
get rid of all the repeated Question queries on the bug page.
3183
        """
12534.1.2 by Robert Collins
Reuse the milestone cache in bugtasks for the bugtask rows rather than just the bugtask edit forms.
3184
        self.cached_milestone_source.contexts.add(context)
6291.1.3 by Bjorn Tillenius
get rid of all the repeated Question queries on the bug page.
3185
        view = getMultiAdapter(
3186
            (context, self.request),
3187
            name='+bugtasks-and-nominations-table-row')
3188
        view.is_converted_to_question = is_converted_to_question
3189
        view.is_conjoined_slave = is_conjoined_slave
6665.4.1 by Bjorn Tillenius
first naive implementation.
3190
        if IBugTask.providedBy(context):
3191
            view.target_link_title = self.getTargetLinkTitle(context.target)
3192
6291.1.6 by Bjorn Tillenius
cache the milestone vocabularies.
3193
        view.edit_view = getMultiAdapter(
3194
            (context, self.request), name='+edit-form')
12599.2.1 by j.c.sackett
Eager loading apparatus in place.
3195
        view.milestone_source = self.cached_milestone_source
6291.1.6 by Bjorn Tillenius
cache the milestone vocabularies.
3196
        view.edit_view.milestone_source = self.cached_milestone_source
6291.1.8 by Bjorn Tillenius
cache bug.isSubscribed(useelf.ser)
3197
        view.edit_view.user_is_subscribed = self.user_is_subscribed
9521.3.1 by Tom Berger
patch from allenap
3198
        # Hint to optimize when there are many bugtasks.
3199
        view.many_bugtasks = self.many_bugtasks
6291.1.3 by Bjorn Tillenius
get rid of all the repeated Question queries on the bug page.
3200
        return view
12393.30.7 by Brad Crittenden
Support new structural subscriptions for IProjectGroup
3201
6291.1.3 by Bjorn Tillenius
get rid of all the repeated Question queries on the bug page.
3202
    def getBugTaskAndNominationViews(self):
3203
        """Return the IBugTasks and IBugNominations views for this bug.
3204
3205
        Returns a list of views, sorted by the context's targetname,
3206
        with upstream tasks sorted before distribution tasks, and
3207
        nominations sorted after tasks. Approved nominations are not
3208
        included in the returned results.
3614.1.68 by Brad Bollenbach
reapply MaloneReleaseManagement
3209
        """
3210
        bug = self.context
6665.4.5 by Bjorn Tillenius
cache the list of bug tasks.
3211
        bugtasks = self.bugtasks
3614.1.68 by Brad Bollenbach
reapply MaloneReleaseManagement
3212
3213
        upstream_tasks = [
3214
            bugtask for bugtask in bugtasks
12799.1.2 by Ian Booth
Rework implementation and do unit tests instead of doc tests
3215
            if bugtask.product or bugtask.productseries]
3614.1.68 by Brad Bollenbach
reapply MaloneReleaseManagement
3216
3217
        distro_tasks = [
3218
            bugtask for bugtask in bugtasks
12799.1.2 by Ian Booth
Rework implementation and do unit tests instead of doc tests
3219
            if bugtask.distribution or bugtask.distroseries]
3614.1.68 by Brad Bollenbach
reapply MaloneReleaseManagement
3220
3221
        upstream_tasks.sort(key=_by_targetname)
3222
        distro_tasks.sort(key=_by_targetname)
3223
        all_bugtasks = upstream_tasks + distro_tasks
3224
6291.1.3 by Bjorn Tillenius
get rid of all the repeated Question queries on the bug page.
3225
        # Cache whether the bug was converted to a question, since
3226
        # bug.getQuestionCreatedFromBug issues a db query each time it
3227
        # is called.
3228
        is_converted_to_question = bug.getQuestionCreatedFromBug() is not None
3614.1.68 by Brad Bollenbach
reapply MaloneReleaseManagement
3229
        # Insert bug nominations in between the appropriate tasks.
6291.1.3 by Bjorn Tillenius
get rid of all the repeated Question queries on the bug page.
3230
        bugtask_and_nomination_views = []
6291.1.2 by Bjorn Tillenius
get rid of all the repeated BugNomination queries.
3231
        # Having getNominations() get the list of bug nominations each
3232
        # time it gets called in the for loop is expensive. Get the
3233
        # nominations here, so we can pass it to getNominations() later
3234
        # on.
3235
        nominations = list(bug.getNominations())
12482.1.5 by Robert Collins
Eager load task and nomination owners too.
3236
        # Eager load validity for all the persons we know of that will be
3237
        # displayed.
12482.1.6 by Robert Collins
IDS -> ids and drop unneeded ILaunchBag import.
3238
        ids = set(map(attrgetter('ownerID'), nominations))
3239
        ids.discard(None)
3240
        if ids:
12482.1.5 by Robert Collins
Eager load task and nomination owners too.
3241
            list(getUtility(IPersonSet).getPrecachedPersonsFromIDs(
12482.1.6 by Robert Collins
IDS -> ids and drop unneeded ILaunchBag import.
3242
                ids, need_validity=True))
6385.6.1 by Bjorn Tillenius
pass in a cache to getConjoinedBugTask, so it doesn't have to loop over the whole list every time.
3243
3244
        # Build a cache we can pass on to getConjoinedMaster(), so that
3245
        # it doesn't have to iterate over all the bug tasks in each loop
3246
        # iteration.
6856.2.16 by Gavin Panella
Move getBugTasksByPackageName from BugTask to Bug. Discussed with BjornT.
3247
        bugtasks_by_package = bug.getBugTasksByPackageName(all_bugtasks)
6385.6.1 by Bjorn Tillenius
pass in a cache to getConjoinedBugTask, so it doesn't have to loop over the whole list every time.
3248
3614.1.68 by Brad Bollenbach
reapply MaloneReleaseManagement
3249
        for bugtask in all_bugtasks:
6385.6.1 by Bjorn Tillenius
pass in a cache to getConjoinedBugTask, so it doesn't have to loop over the whole list every time.
3250
            conjoined_master = bugtask.getConjoinedMaster(
3251
                bugtasks, bugtasks_by_package)
6291.1.3 by Bjorn Tillenius
get rid of all the repeated Question queries on the bug page.
3252
            view = self._getTableRowView(
3253
                bugtask, is_converted_to_question,
3254
                conjoined_master is not None)
3255
            bugtask_and_nomination_views.append(view)
3614.1.68 by Brad Bollenbach
reapply MaloneReleaseManagement
3256
            target = bugtask.product or bugtask.distribution
3257
            if not target:
3258
                continue
3259
6291.1.2 by Bjorn Tillenius
get rid of all the repeated BugNomination queries.
3260
            target_nominations = bug.getNominations(
3261
                target, nominations=nominations)
6291.1.5 by Bjorn Tillenius
use extend() instead of +=
3262
            bugtask_and_nomination_views.extend(
6291.1.3 by Bjorn Tillenius
get rid of all the repeated Question queries on the bug page.
3263
                self._getTableRowView(
3264
                    nomination, is_converted_to_question, False)
6291.1.2 by Bjorn Tillenius
get rid of all the repeated BugNomination queries.
3265
                for nomination in target_nominations
11411.7.29 by j.c.sackett
Lint fixes.
3266
                if nomination.status != BugNominationStatus.APPROVED)
3614.1.68 by Brad Bollenbach
reapply MaloneReleaseManagement
3267
6291.1.3 by Bjorn Tillenius
get rid of all the repeated Question queries on the bug page.
3268
        return bugtask_and_nomination_views
3614.1.68 by Brad Bollenbach
reapply MaloneReleaseManagement
3269
6856.2.1 by Gavin Panella
Change currentBugTask() to current_bugtask.
3270
    @property
3271
    def current_bugtask(self):
4974.2.1 by Curtis Hovey
Fixes per pylint.
3272
        """Return the current `IBugTask`.
3614.1.68 by Brad Bollenbach
reapply MaloneReleaseManagement
3273
3274
        'current' is determined by simply looking in the ILaunchBag utility.
3275
        """
3276
        return getUtility(ILaunchBag).bugtask
3277
4896.4.11 by Graham Binns
Bug task and nomination edit forms and links are now only displayed where an ICveSet has not been traversed in the request.
3278
    def displayAlsoAffectsLinks(self):
3279
        """Return True if the Also Affects links should be displayed."""
4896.4.14 by Graham Binns
Added comments to explain that forms and links for bugtasks and nominations will be hidden in CVE contexts.
3280
        # Hide the links when the bug is viewed in a CVE context
4896.4.11 by Graham Binns
Bug task and nomination edit forms and links are now only displayed where an ICveSet has not been traversed in the request.
3281
        return self.request.getNearest(ICveSet) == (None, None)
3282
11582.2.2 by Robert Collins
Probably broken, but takes 46 queries off of a baseline BugTask:+index.
3283
    @cachedproperty
7175.1.19 by Martin Albisetti
Fixed indentation and renamed a method for clearness
3284
    def current_user_affected_status(self):
7175.2.1 by Tom Berger
move the me-too control under the affects table
3285
        """Is the current user marked as affected by this bug?"""
3286
        return self.context.isUserAffected(self.user)
3287
3288
    @property
8971.26.2 by Gavin Panella
New property current_user_affected_js_status, and tests.
3289
    def current_user_affected_js_status(self):
3290
        """A javascript literal indicating if the user is affected."""
3291
        affected = self.current_user_affected_status
3292
        if affected is None:
3293
            return 'null'
3294
        elif affected:
3295
            return 'true'
7175.2.1 by Tom Berger
move the me-too control under the affects table
3296
        else:
8971.26.2 by Gavin Panella
New property current_user_affected_js_status, and tests.
3297
            return 'false'
7175.2.1 by Tom Berger
move the me-too control under the affects table
3298
10015.1.2 by Gavin Panella
New view property other_users_affected_count.
3299
    @property
3300
    def other_users_affected_count(self):
3301
        """The number of other users affected by this bug."""
3302
        if self.current_user_affected_status:
3303
            return self.context.users_affected_count - 1
3304
        else:
3305
            return self.context.users_affected_count
3306
10015.1.3 by Gavin Panella
New view property affected_statement.
3307
    @property
3308
    def affected_statement(self):
10015.1.22 by Gavin Panella
Comment that _getSourceNames(), affected_statement and anon_affected_statement should all output similar strings.
3309
        """The default "this bug affects" statement to show.
3310
3311
        The outputs of this method should be mirrored in
3312
        MeTooChoiceSource._getSourceNames() (Javascript).
3313
        """
10015.1.3 by Gavin Panella
New view property affected_statement.
3314
        if self.other_users_affected_count == 1:
10015.1.8 by Gavin Panella
Make affected_statement different when a user has explicitly said they are not affected, and when they have not.
3315
            if self.current_user_affected_status is None:
3316
                return "This bug affects 1 person. Does this bug affect you?"
3317
            elif self.current_user_affected_status:
10015.1.7 by Gavin Panella
Remove trailing full-stops from the affected statement.
3318
                return "This bug affects you and 1 other person"
10015.1.3 by Gavin Panella
New view property affected_statement.
3319
            else:
10015.1.8 by Gavin Panella
Make affected_statement different when a user has explicitly said they are not affected, and when they have not.
3320
                return "This bug affects 1 person, but not you"
10015.1.3 by Gavin Panella
New view property affected_statement.
3321
        elif self.other_users_affected_count > 1:
10015.1.8 by Gavin Panella
Make affected_statement different when a user has explicitly said they are not affected, and when they have not.
3322
            if self.current_user_affected_status is None:
10015.1.3 by Gavin Panella
New view property affected_statement.
3323
                return (
3324
                    "This bug affects %d people. Does this bug "
3325
                    "affect you?" % (self.other_users_affected_count))
10015.1.8 by Gavin Panella
Make affected_statement different when a user has explicitly said they are not affected, and when they have not.
3326
            elif self.current_user_affected_status:
3327
                return "This bug affects you and %d other people" % (
3328
                    self.other_users_affected_count)
3329
            else:
3330
                return "This bug affects %d people, but not you" % (
3331
                    self.other_users_affected_count)
10015.1.3 by Gavin Panella
New view property affected_statement.
3332
        else:
10015.1.8 by Gavin Panella
Make affected_statement different when a user has explicitly said they are not affected, and when they have not.
3333
            if self.current_user_affected_status is None:
3334
                return "Does this bug affect you?"
3335
            elif self.current_user_affected_status:
3336
                return "This bug affects you"
3337
            else:
10015.1.10 by Gavin Panella
Say "doesn't" instead of "does not".
3338
                return "This bug doesn't affect you"
10015.1.3 by Gavin Panella
New view property affected_statement.
3339
10015.1.18 by Gavin Panella
New view property anon_affected_statement.
3340
    @property
3341
    def anon_affected_statement(self):
10015.1.22 by Gavin Panella
Comment that _getSourceNames(), affected_statement and anon_affected_statement should all output similar strings.
3342
        """The "this bug affects" statement to show to anonymous users.
3343
3344
        The outputs of this method should be mirrored in
3345
        MeTooChoiceSource._getSourceNames() (Javascript).
3346
        """
10015.1.18 by Gavin Panella
New view property anon_affected_statement.
3347
        if self.context.users_affected_count == 1:
3348
            return "This bug affects 1 person"
3349
        elif self.context.users_affected_count > 1:
3350
            return "This bug affects %d people" % (
3351
                self.context.users_affected_count)
3352
        else:
3353
            return None
3354
3614.1.68 by Brad Bollenbach
reapply MaloneReleaseManagement
3355
10888.3.1 by Graham Binns
Moved BugWatch.getLastErrorMessage() into view code, where it actually belongs.
3356
class BugTaskTableRowView(LaunchpadView, BugTaskBugWatchMixin):
3614.1.68 by Brad Bollenbach
reapply MaloneReleaseManagement
3357
    """Browser class for rendering a bugtask row on the bug page."""
2903.1.231 by Matthew Paul Thomas
Fixes bug 75469, with a pagetest.
3358
6138.3.2 by Bjorn Tillenius
calculate whether bugtask is a conjoined one, and pass it on to the view.
3359
    is_conjoined_slave = None
6291.1.3 by Bjorn Tillenius
get rid of all the repeated Question queries on the bug page.
3360
    is_converted_to_question = None
6665.4.1 by Bjorn Tillenius
first naive implementation.
3361
    target_link_title = None
9521.3.1 by Tom Berger
patch from allenap
3362
    many_bugtasks = False
6138.3.2 by Bjorn Tillenius
calculate whether bugtask is a conjoined one, and pass it on to the view.
3363
12534.1.2 by Robert Collins
Reuse the milestone cache in bugtasks for the bugtask rows rather than just the bugtask edit forms.
3364
    def __init__(self, context, request):
3365
        super(BugTaskTableRowView, self).__init__(context, request)
3366
        self.milestone_source = MilestoneVocabulary
3367
4002.7.21 by Matthew Paul Thomas
Fixes bug 54801 (Shouldn't be possible to change status/assignee/importance/milestone of a duplicate bug).
3368
    def canSeeTaskDetails(self):
4002.7.24 by Matthew Paul Thomas
Renames sampledata ubuntu-project to apache, and ubuntu-product to tomcat. Reverts to using the project name (not targetname) in the bugactivity table.
3369
        """Whether someone can see a task's status details.
4002.7.50 by Matthew Paul Thomas
Makes fixes from BjornT's second review, and fixes tests.
3370
4755.1.36 by Curtis Hovey
Added a non-edit rule for bugtasks when a question was created from a bug. Cleanup trailing spaces.
3371
        Return True if this is not a conjoined task, and the bug is
3372
        not a duplicate, and a question was not made from this report.
3373
        It is independent of whether they can *change* the status; you
3374
        need to expand the details to see any milestone set.
4002.7.21 by Matthew Paul Thomas
Fixes bug 54801 (Shouldn't be possible to change status/assignee/importance/milestone of a duplicate bug).
3375
        """
6138.3.2 by Bjorn Tillenius
calculate whether bugtask is a conjoined one, and pass it on to the view.
3376
        assert self.is_conjoined_slave is not None, (
3377
            'is_conjoined_slave should be set before rendering the page.')
6291.1.3 by Bjorn Tillenius
get rid of all the repeated Question queries on the bug page.
3378
        assert self.is_converted_to_question is not None, (
3379
            'is_converted_to_question should be set before rendering the'
3380
            ' page.')
4896.4.13 by Graham Binns
Function renaming.
3381
        return (self.displayEditForm() and
6138.3.2 by Bjorn Tillenius
calculate whether bugtask is a conjoined one, and pass it on to the view.
3382
                not self.is_conjoined_slave and
4755.1.36 by Curtis Hovey
Added a non-edit rule for bugtasks when a question was created from a bug. Cleanup trailing spaces.
3383
                self.context.bug.duplicateof is None and
6291.1.3 by Bjorn Tillenius
get rid of all the repeated Question queries on the bug page.
3384
                not self.is_converted_to_question)
4002.7.21 by Matthew Paul Thomas
Fixes bug 54801 (Shouldn't be possible to change status/assignee/importance/milestone of a duplicate bug).
3385
2903.1.231 by Matthew Paul Thomas
Fixes bug 75469, with a pagetest.
3386
    def getTaskRowCSSClass(self):
4002.7.24 by Matthew Paul Thomas
Renames sampledata ubuntu-project to apache, and ubuntu-product to tomcat. Reverts to using the project name (not targetname) in the bugactivity table.
3387
        """The appropriate CSS class for the row in the Affects table.
4002.7.50 by Matthew Paul Thomas
Makes fixes from BjornT's second review, and fixes tests.
3388
2903.1.231 by Matthew Paul Thomas
Fixes bug 75469, with a pagetest.
3389
        Currently this consists solely of highlighting the current context.
3390
        """
3391
        bugtask = self.context
3392
        if bugtask == getUtility(ILaunchBag).bugtask:
3393
            return 'highlight'
3394
        else:
2903.1.232 by Matthew Paul Thomas
Removes useless class="" attribute, and tightens the pagetest to match.
3395
            return None
2903.1.231 by Matthew Paul Thomas
Fixes bug 75469, with a pagetest.
3396
3614.1.68 by Brad Bollenbach
reapply MaloneReleaseManagement
3397
    def shouldIndentTask(self):
3398
        """Should this task be indented in the task listing on the bug page?
3399
3400
        Returns True or False.
3401
        """
13479.2.7 by William Grant
Rip them out of browser.bugtask.
3402
        if self.context.productseries or self.context.distroseries:
3403
            return True
3404
        return False
3614.1.68 by Brad Bollenbach
reapply MaloneReleaseManagement
3405
3406
    def taskLink(self):
3407
        """Return the proper link to the bugtask whether it's editable."""
3408
        user = getUtility(ILaunchBag).user
3409
        bugtask = self.context
3691.373.6 by Christian Reis
Move check_permission into webapp.authorization
3410
        if check_permission('launchpad.Edit', user):
3614.1.68 by Brad Bollenbach
reapply MaloneReleaseManagement
3411
            return canonical_url(bugtask) + "/+editstatus"
3412
        else:
3413
            return canonical_url(bugtask) + "/+viewstatus"
3414
4285.2.1 by Mark Shuttleworth
Massive renaming of distrorelease to distroseries
3415
    def _getSeriesTargetNameHelper(self, bugtask):
3416
        """Return the short name of bugtask's targeted series."""
13479.2.7 by William Grant
Rip them out of browser.bugtask.
3417
        series = bugtask.distroseries or bugtask.productseries
3418
        if not series:
3419
            return None
3420
        return series.name.capitalize()
3614.1.79 by Brad Bollenbach
add UI for conjoined tasks
3421
4285.2.1 by Mark Shuttleworth
Massive renaming of distrorelease to distroseries
3422
    def getSeriesTargetName(self):
3423
        """Get the series to which this task is targeted."""
3424
        return self._getSeriesTargetNameHelper(self.context)
3614.1.91 by Bjorn Tillenius
make the release task editable in the UI, instead of the generic task.
3425
3426
    def getConjoinedMasterName(self):
3427
        """Get the conjoined master's name for displaying."""
4285.2.1 by Mark Shuttleworth
Massive renaming of distrorelease to distroseries
3428
        return self._getSeriesTargetNameHelper(self.context.conjoined_master)
3614.1.91 by Bjorn Tillenius
make the release task editable in the UI, instead of the generic task.
3429
4002.7.50 by Matthew Paul Thomas
Makes fixes from BjornT's second review, and fixes tests.
3430
    @property
3431
    def bugtask_icon(self):
4002.7.42 by Matthew Paul Thomas
Fixes from jamesh's review.
3432
        """Which icon should be shown for the task, if any?"""
8490.3.70 by Martin Albisetti
Address review comments
3433
        return getAdapter(self.context, IPathAdapter, 'image').sprite_css()
3811.2.3 by Bjorn Tillenius
initial go at making the search on the Bugs front page work.
3434
4896.4.13 by Graham Binns
Function renaming.
3435
    def displayEditForm(self):
4896.4.11 by Graham Binns
Bug task and nomination edit forms and links are now only displayed where an ICveSet has not been traversed in the request.
3436
        """Return true if the BugTask edit form should be shown."""
4896.4.14 by Graham Binns
Added comments to explain that forms and links for bugtasks and nominations will be hidden in CVE contexts.
3437
        # Hide the edit form when the bug is viewed in a CVE context
4896.4.11 by Graham Binns
Bug task and nomination edit forms and links are now only displayed where an ICveSet has not been traversed in the request.
3438
        return self.request.getNearest(ICveSet) == (None, None)
3439
8255.14.1 by Edwin Grubbs
Re-added code for inline bugtask assignee picker widget from
3440
    @property
8230.3.1 by Tom Berger
interim commit so that rockstar can have a look
3441
    def status_widget_items(self):
8230.3.14 by Tom Berger
handle anonymous users and clients without js properly
3442
        """The available status items as JSON."""
3443
        if self.user is not None:
9055.5.1 by Guilherme Salgado
Remove IDBSchema and some unused code related to it
3444
            # We shouldn't have to build our vocabulary out of (item.title,
3445
            # item) tuples -- iterating over an EnumeratedType gives us
3446
            # ITokenizedTerms that we could use. However, the terms generated
9055.5.2 by Guilherme Salgado
Remove XXXs
3447
            # by EnumeratedType have their name as the token and here we need
3448
            # the title as the token for backwards compatibility.
9055.5.1 by Guilherme Salgado
Remove IDBSchema and some unused code related to it
3449
            status_items = [
3450
                (item.title, item) for item in BugTaskStatus.items
10680.2.1 by Abel Deuring
Add a new bug task status 'Expired'; the bug task expiration script sets expired bug tasks to this status; related adjustments to the bug task views
3451
                if item not in (BugTaskStatus.UNKNOWN,
3452
                                BugTaskStatus.EXPIRED)]
8230.3.14 by Tom Berger
handle anonymous users and clients without js properly
3453
3454
            disabled_items = [status for status in BugTaskStatus.items
3455
                if not self.context.canTransitionToStatus(status, self.user)]
3456
3457
            items = vocabulary_to_choice_edit_items(
9055.5.1 by Guilherme Salgado
Remove IDBSchema and some unused code related to it
3458
                SimpleVocabulary.fromItems(status_items),
8230.3.14 by Tom Berger
handle anonymous users and clients without js properly
3459
                css_class_prefix='status',
3460
                disabled_items=disabled_items)
8230.3.18 by Tom Berger
return the widget items
3461
        else:
3462
            items = '[]'
3463
3464
        return items
8230.3.16 by Tom Berger
merge changes from rocketfuel and resolve conflicts
3465
8450.4.1 by Tom Berger
make it possible to edit a bugtask importance inline
3466
    @property
3467
    def importance_widget_items(self):
3468
        """The available status items as JSON."""
3469
        if self.user is not None:
9055.5.1 by Guilherme Salgado
Remove IDBSchema and some unused code related to it
3470
            # We shouldn't have to build our vocabulary out of (item.title,
3471
            # item) tuples -- iterating over an EnumeratedType gives us
3472
            # ITokenizedTerms that we could use. However, the terms generated
9055.5.2 by Guilherme Salgado
Remove XXXs
3473
            # by EnumeratedType have their name as the token and here we need
3474
            # the title as the token for backwards compatibility.
9055.5.1 by Guilherme Salgado
Remove IDBSchema and some unused code related to it
3475
            importance_items = [
3476
                (item.title, item) for item in BugTaskImportance.items
3477
                if item != BugTaskImportance.UNKNOWN]
8450.4.1 by Tom Berger
make it possible to edit a bugtask importance inline
3478
3479
            items = vocabulary_to_choice_edit_items(
9055.5.1 by Guilherme Salgado
Remove IDBSchema and some unused code related to it
3480
                SimpleVocabulary.fromItems(importance_items),
8450.4.1 by Tom Berger
make it possible to edit a bugtask importance inline
3481
                css_class_prefix='importance')
3482
        else:
3483
            items = '[]'
3484
3485
        return items
3486
11618.1.2 by Deryck Hodge
Remove a couple queries by better handling of Milestone vocabulary.
3487
    @cachedproperty
3488
    def _visible_milestones(self):
3489
        """The visible milestones for this context."""
12534.1.2 by Robert Collins
Reuse the milestone cache in bugtasks for the bugtask rows rather than just the bugtask edit forms.
3490
        return self.milestone_source(self.context).visible_milestones
11618.1.2 by Deryck Hodge
Remove a couple queries by better handling of Milestone vocabulary.
3491
8450.3.6 by Tom Berger
interim commit with working UI sans removal and add icon
3492
    @property
3493
    def milestone_widget_items(self):
3494
        """The available milestone items as JSON."""
3495
        if self.user is not None:
3496
            items = vocabulary_to_choice_edit_items(
11618.1.2 by Deryck Hodge
Remove a couple queries by better handling of Milestone vocabulary.
3497
                self._visible_milestones,
8877.3.6 by Tom Berger
inline value_fn
3498
                value_fn=lambda item: canonical_url(
3499
                    item, request=IWebServiceClientRequest(self.request)))
8877.3.9 by Tom Berger
edit milestones using a nullable choice edit
3500
            items.append({
3501
                "name": "Remove milestone",
3502
                "disabled": False,
3503
                "value": None})
8450.3.6 by Tom Berger
interim commit with working UI sans removal and add icon
3504
        else:
3505
            items = '[]'
3506
3507
        return items
3508
11582.2.2 by Robert Collins
Probably broken, but takes 46 queries off of a baseline BugTask:+index.
3509
    @cachedproperty
8877.3.12 by Tom Berger
dont show the milestone widget if there are no targetable milestones
3510
    def target_has_milestones(self):
11618.1.5 by Deryck Hodge
Add a comment to clarify why len on the list is ok.
3511
        """Are there any milestones we can target?
3512
3513
        We always look up all milestones, so there's no harm
3514
        using len on the list here and avoid the COUNT query.
3515
        """
11618.1.2 by Deryck Hodge
Remove a couple queries by better handling of Milestone vocabulary.
3516
        return len(self._visible_milestones) > 0
8877.3.12 by Tom Berger
dont show the milestone widget if there are no targetable milestones
3517
8255.14.1 by Edwin Grubbs
Re-added code for inline bugtask assignee picker widget from
3518
    def bugtask_canonical_url(self):
3519
        """Return the canonical url for the bugtask."""
3520
        return canonical_url(self.context)
3521
3522
    @property
8450.4.1 by Tom Berger
make it possible to edit a bugtask importance inline
3523
    def user_can_edit_importance(self):
3524
        """Can the user edit the Importance field?
3525
3526
        If yes, return True, otherwise return False.
3527
        """
3528
        return self.context.userCanEditImportance(self.user)
3529
9141.6.1 by Tom Berger
dont allow users who shouldnt edit milestone edit it using the ajax control
3530
    @property
13025.2.1 by William Grant
Only install the bugtask assignee picker if the user is logged in.
3531
    def user_can_edit_assignee(self):
3532
        """Can the user edit the Milestone field?
3533
3534
        If yes, return True, otherwise return False.
3535
        """
3536
        return self.user is not None
3537
3538
    @property
9141.6.1 by Tom Berger
dont allow users who shouldnt edit milestone edit it using the ajax control
3539
    def user_can_edit_milestone(self):
3540
        """Can the user edit the Milestone field?
3541
3542
        If yes, return True, otherwise return False.
3543
        """
3544
        return self.context.userCanEditMilestone(self.user)
3545
12117.2.1 by Edwin Grubbs
Fixed add milestone icon to be ajaxified just like the text on the bugtask.
3546
    @property
3547
    def style_for_add_milestone(self):
3548
        if self.context.milestone is None:
3549
            return ''
3550
        else:
3551
            return 'display: none'
3552
3553
    @property
3554
    def style_for_edit_milestone(self):
3555
        if self.context.milestone is None:
3556
            return 'display: none'
3557
        else:
3558
            return ''
3559
8935.1.1 by Tom Berger
Set the controls of the inline bugtask form when setting status and importance. Also produce the initialization values for the bugtask row in a saner way, using simplejson from the view code.
3560
    def js_config(self):
3561
        """Configuration for the JS widgets on the row, JSON-serialized."""
10788.5.2 by Abel Deuring
ordinary users can (un)assign a bug task only to hemselves and their teams
3562
        assignee_vocabulary = get_assignee_vocabulary(self.context)
3563
        # Display the search field only if the user can set any person
3564
        # or team
3565
        user = getUtility(ILaunchBag).user
3566
        hide_assignee_team_selection = (
3567
            not self.context.userCanSetAnyAssignee(user) and
3568
            (user is None or user.teams_participated_in.count() == 0))
8935.1.1 by Tom Berger
Set the controls of the inline bugtask form when setting status and importance. Also produce the initialization values for the bugtask row in a saner way, using simplejson from the view code.
3569
        return dumps({
3570
            'row_id': 'tasksummary%s' % self.context.id,
3571
            'bugtask_path': '/'.join(
3572
                [''] + canonical_url(self.context).split('/')[3:]),
3573
            'prefix': get_prefix(self.context),
13465.1.5 by Ian Booth
Move all assignme and remove button behaviour into PersonPicker and refactor tests
3574
            'assignee_value': self.context.assignee
3575
                and self.context.assignee.name,
13409.2.4 by Ian Booth
Add dynamic picker text
3576
            'assignee_is_team': self.context.assignee
3577
                and self.context.assignee.is_team,
10788.5.2 by Abel Deuring
ordinary users can (un)assign a bug task only to hemselves and their teams
3578
            'assignee_vocabulary': assignee_vocabulary,
3579
            'hide_assignee_team_selection': hide_assignee_team_selection,
3580
            'user_can_unassign': self.context.userCanUnassign(user),
9087.5.1 by Tom Berger
interim commit so that i can pull in the latest trunk
3581
            'target_is_product': IProduct.providedBy(self.context.target),
8935.1.1 by Tom Berger
Set the controls of the inline bugtask form when setting status and importance. Also produce the initialization values for the bugtask row in a saner way, using simplejson from the view code.
3582
            'status_widget_items': self.status_widget_items,
3583
            'status_value': self.context.status.title,
3584
            'importance_widget_items': self.importance_widget_items,
3585
            'importance_value': self.context.importance.title,
8877.3.2 by Tom Berger
merge changes from rocketfuel, resolve conflicts and bring up-to-date
3586
            'milestone_widget_items': self.milestone_widget_items,
3587
            'milestone_value': (self.context.milestone and
8877.3.9 by Tom Berger
edit milestones using a nullable choice edit
3588
                                canonical_url(
3589
                                    self.context.milestone,
3590
                                    request=IWebServiceClientRequest(
3591
                                        self.request)) or
3592
                                None),
13025.2.1 by William Grant
Only install the bugtask assignee picker if the user is logged in.
3593
            'user_can_edit_assignee': self.user_can_edit_assignee,
9141.6.1 by Tom Berger
dont allow users who shouldnt edit milestone edit it using the ajax control
3594
            'user_can_edit_milestone': self.user_can_edit_milestone,
9141.2.1 by Tom Berger
dont create ajax edit widgets for status and importance if a bugtask is controlled by a bugwatch.
3595
            'user_can_edit_status': not self.context.bugwatch,
3596
            'user_can_edit_importance': (
3597
                self.user_can_edit_importance and
3598
                not self.context.bugwatch)})
8450.4.1 by Tom Berger
make it possible to edit a bugtask importance inline
3599
3811.2.3 by Bjorn Tillenius
initial go at making the search on the Bugs front page work.
3600
3601
class BugsBugTaskSearchListingView(BugTaskSearchListingView):
3811.2.8 by Bjorn Tillenius
clean up.
3602
    """Search all bug reports."""
3811.2.3 by Bjorn Tillenius
initial go at making the search on the Bugs front page work.
3603
4682.1.3 by Christian Reis
Fix the confusion left behind when IBugTask.targetname was half removed. Update sampledata to include the new targetname content (which is actually bugtargetdisplayname) and fix up tests and code.
3604
    columns_to_show = ["id", "summary", "bugtargetdisplayname",
7675.480.1 by Tom Berger
display the bug heat flames in bug listings
3605
                       "importance", "status", "heat"]
3811.2.3 by Bjorn Tillenius
initial go at making the search on the Bugs front page work.
3606
    schema = IFrontPageBugTaskSearch
4584.2.1 by Graham Binns
Refactored BugTaskSearchListingView to inherit from LaunchpadFormView rather than LaunchpadView. Updated ZCML, templates and descendant classes accordingly.
3607
    custom_widget('scope', ProjectScopeWidget)
9559.3.6 by Deryck Hodge
Update after review to get the page_title/label dance right.
3608
    page_title = 'Search'
3811.2.3 by Bjorn Tillenius
initial go at making the search on the Bugs front page work.
3609
3610
    def initialize(self):
4974.2.1 by Curtis Hovey
Fixes per pylint.
3611
        """Initialize the view for the request."""
3811.2.3 by Bjorn Tillenius
initial go at making the search on the Bugs front page work.
3612
        BugTaskSearchListingView.initialize(self)
4318.3.6 by Gavin Panella
Add redirection functionality.
3613
        if not self._isRedirected():
3614
            self._redirectToSearchContext()
3811.2.4 by Bjorn Tillenius
make searching from the Bugs page work properly.
3615
3616
    def _redirectToSearchContext(self):
3811.2.8 by Bjorn Tillenius
clean up.
3617
        """Check wether a target was given and redirect to it.
3618
3619
        All the URL parameters will be passed on to the target's +bugs
3620
        page.
3621
3622
        If the target widget contains errors, redirect to the front page
3623
        which will handle the error.
3624
        """
3811.2.3 by Bjorn Tillenius
initial go at making the search on the Bugs front page work.
3625
        try:
4584.2.1 by Graham Binns
Refactored BugTaskSearchListingView to inherit from LaunchpadFormView rather than LaunchpadView. Updated ZCML, templates and descendant classes accordingly.
3626
            search_target = self.widgets['scope'].getInputValue()
3811.2.4 by Bjorn Tillenius
make searching from the Bugs page work properly.
3627
        except InputErrors:
3628
            query_string = self.request['QUERY_STRING']
3629
            bugs_url = "%s?%s" % (canonical_url(self.context), query_string)
3630
            self.request.response.redirect(bugs_url)
3811.2.3 by Bjorn Tillenius
initial go at making the search on the Bugs front page work.
3631
        else:
3811.2.14 by Bjorn Tillenius
make use of the ProjectScopeWidget.
3632
            if search_target is not None:
3811.2.4 by Bjorn Tillenius
make searching from the Bugs page work properly.
3633
                query_string = self.request['QUERY_STRING']
3634
                search_url = "%s/+bugs?%s" % (
3635
                    canonical_url(search_target), query_string)
3636
                self.request.response.redirect(search_url)
3811.2.3 by Bjorn Tillenius
initial go at making the search on the Bugs front page work.
3637
3638
    def getSearchPageHeading(self):
4974.2.1 by Curtis Hovey
Fixes per pylint.
3639
        """Return the heading to search all Bugs."""
3811.2.3 by Bjorn Tillenius
initial go at making the search on the Bugs front page work.
3640
        return "Search all bug reports"
3973.1.46 by Steve Alexander
add bugtask SOP for improved bugtask page heading
3641
9559.3.4 by Deryck Hodge
Add a missing page_title, causing tests to fail.
3642
    @property
9559.3.6 by Deryck Hodge
Update after review to get the page_title/label dance right.
3643
    def label(self):
9559.3.4 by Deryck Hodge
Add a missing page_title, causing tests to fail.
3644
        return self.getSearchPageHeading()
3645
3973.1.46 by Steve Alexander
add bugtask SOP for improved bugtask page heading
3646
5474.3.7 by Edwin Grubbs
Working ObjectPrivacy implementation
3647
class BugTaskPrivacyAdapter:
3648
    """Provides `IObjectPrivacy` for `IBugTask`."""
3649
3650
    implements(IObjectPrivacy)
3651
3652
    def __init__(self, context):
3653
        self.context = context
3654
3655
    @property
3656
    def is_private(self):
3657
        """Return True if the bug is private, otherwise False."""
3658
        return self.context.bug.private
3659
5474.3.10 by Edwin Grubbs
Working display using HasBadgeBase
3660
4755.1.48 by Curtis Hovey
Changes per review. Anonther massive restructuring.
3661
class BugTaskCreateQuestionView(LaunchpadFormView):
4755.1.24 by Curtis Hovey
Update the view and template. Final text and labels are needed to
3662
    """View for creating a question from a bug."""
4755.1.48 by Curtis Hovey
Changes per review. Anonther massive restructuring.
3663
    schema = ICreateQuestionFromBugTaskForm
3664
3665
    def setUpFields(self):
3666
        """See `LaunchpadFormView`."""
3667
        LaunchpadFormView.setUpFields(self)
3668
        if not self.can_be_a_question:
3669
            self.form_fields = self.form_fields.omit('comment')
4755.1.24 by Curtis Hovey
Update the view and template. Final text and labels are needed to
3670
3671
    @property
3672
    def next_url(self):
3673
        """See `LaunchpadFormView`."""
3674
        return canonical_url(self.context)
3675
3676
    @property
3677
    def can_be_a_question(self):
3678
        """Return True if this bug can become a question, otherwise False."""
3679
        return self.context.bug.canBeAQuestion()
3680
4755.1.43 by Curtis Hovey
Revisions pre review.
3681
    @action('Convert this Bug into a Question', name='create')
4755.1.24 by Curtis Hovey
Update the view and template. Final text and labels are needed to
3682
    def create_action(self, action, data):
3683
        """Create a question from this bug and set this bug to Invalid.
4755.1.43 by Curtis Hovey
Revisions pre review.
3684
4755.1.24 by Curtis Hovey
Update the view and template. Final text and labels are needed to
3685
        The bugtask's status will be set to Invalid. The question
3686
        will be linked to this bug.
4755.1.43 by Curtis Hovey
Revisions pre review.
3687
4755.1.24 by Curtis Hovey
Update the view and template. Final text and labels are needed to
3688
        A question will not be created if a question was previously created,
3689
        the pillar does not use Launchpad to track bugs, or there is more
3690
        than one valid bugtask.
3691
        """
4755.1.43 by Curtis Hovey
Revisions pre review.
3692
        if not self.context.bug.canBeAQuestion():
4755.1.24 by Curtis Hovey
Update the view and template. Final text and labels are needed to
3693
            self.request.response.addNotification(
4755.1.43 by Curtis Hovey
Revisions pre review.
3694
                'This bug could not be converted into a question.')
4755.1.24 by Curtis Hovey
Update the view and template. Final text and labels are needed to
3695
            return
3696
4755.1.43 by Curtis Hovey
Revisions pre review.
3697
        comment = data.get('comment', None)
3698
        self.context.bug.convertToQuestion(self.user, comment=comment)
4755.1.39 by Curtis Hovey
Added remove a question created from a question.
3699
9254.4.1 by Abel Deuring
bug-create-question.pt changed to LP3 layout
3700
    label = 'Convert this bug to a question'
3701
9254.4.2 by Abel Deuring
switch to main_only from main_side; replace entry in pagetitles by view property
3702
    page_title = label
3703
4755.1.39 by Curtis Hovey
Added remove a question created from a question.
3704
4755.1.48 by Curtis Hovey
Changes per review. Anonther massive restructuring.
3705
class BugTaskRemoveQuestionView(LaunchpadFormView):
4755.1.39 by Curtis Hovey
Added remove a question created from a question.
3706
    """View for creating a question from a bug."""
4755.1.48 by Curtis Hovey
Changes per review. Anonther massive restructuring.
3707
    schema = IRemoveQuestionFromBugTaskForm
3708
3709
    def setUpFields(self):
3710
        """See `LaunchpadFormView`."""
3711
        LaunchpadFormView.setUpFields(self)
3712
        if not self.has_question:
3713
            self.form_fields = self.form_fields.omit('comment')
4755.1.39 by Curtis Hovey
Added remove a question created from a question.
3714
3715
    @property
3716
    def next_url(self):
3717
        """See `LaunchpadFormView`."""
3718
        return canonical_url(self.context)
3719
3720
    @property
3721
    def has_question(self):
3722
        """Return True if a question was created from this bug, or False."""
3723
        return self.context.bug.getQuestionCreatedFromBug() is not None
3724
4755.1.48 by Curtis Hovey
Changes per review. Anonther massive restructuring.
3725
    @action('Convert Back to Bug', name='remove')
4755.1.39 by Curtis Hovey
Added remove a question created from a question.
3726
    def remove_action(self, action, data):
3727
        """Remove a question from this bug.
4755.1.43 by Curtis Hovey
Revisions pre review.
3728
4755.1.39 by Curtis Hovey
Added remove a question created from a question.
3729
        The question will be unlinked from the bug. The question is not
3730
        altered in any other way; it belongs to the question workflow.
3731
        The bug's bugtasks are editable, though none are changed. Bug
6087.7.2 by Tom Berger
convert bug contact to bug supervisor in template and browser code
3732
        supervisors are responsible for updating the bugtasks.
4755.1.39 by Curtis Hovey
Added remove a question created from a question.
3733
        """
3734
        question = self.context.bug.getQuestionCreatedFromBug()
3735
        if question is None:
3736
            self.request.response.addNotification(
3737
                'This bug does not have a question to remove')
3738
            return
3739
4755.1.43 by Curtis Hovey
Revisions pre review.
3740
        owner_is_subscribed = question.isSubscribed(self.context.bug.owner)
4755.1.39 by Curtis Hovey
Added remove a question created from a question.
3741
        question.unlinkBug(self.context.bug)
3742
        # The question.owner was implicitly unsubscribed when the bug
4755.1.43 by Curtis Hovey
Revisions pre review.
3743
        # was unlinked. We resubscribe the owner if he was subscribed.
3744
        if owner_is_subscribed is True:
5454.1.5 by Tom Berger
record who created each bug subscription, and display the result in the title of the subscriber link
3745
            self.context.bug.subscribe(question.owner, self.user)
5594.1.11 by Maris Fogels
Beautified the addNotification(structured(...)) construct.
3746
        self.request.response.addNotification(
5594.1.29 by Maris Fogels
Rework based reviewer feedback.
3747
            structured(
5594.1.33 by Maris Fogels
Fixed a whitespace bug and some unnecessary URL escaping.
3748
                'Removed Question #%s: <a href="%s">%s<a>.',
3749
                str(question.id),
5594.1.29 by Maris Fogels
Rework based reviewer feedback.
3750
                canonical_url(question),
5594.1.33 by Maris Fogels
Fixed a whitespace bug and some unnecessary URL escaping.
3751
                question.title))
3752
4755.1.48 by Curtis Hovey
Changes per review. Anonther massive restructuring.
3753
        comment = data.get('comment', None)
3754
        if comment is not None:
3755
            self.context.bug.newMessage(
3756
                owner=getUtility(ILaunchBag).user,
3757
                subject=self.context.bug.followup_subject(),
3758
                content=comment)
5254.2.11 by Curtis Hovey
Added the expirable bugtaks view, extended the buglisting to offer date_last_updated,
3759
9292.2.1 by Abel Deuring
bug-remove-question.pt converted to LP3 layout
3760
    @property
3761
    def label(self):
3762
        return ('Bug #%i - Convert this question back to a bug'
3763
                % self.context.bug.id)
3764
3765
    page_title = label
3766
5254.2.11 by Curtis Hovey
Added the expirable bugtaks view, extended the buglisting to offer date_last_updated,
3767
3768
class BugTaskExpirableListingView(LaunchpadView):
3769
    """View for listing Incomplete bugs that can expire."""
11411.7.29 by j.c.sackett
Lint fixes.
3770
5254.2.11 by Curtis Hovey
Added the expirable bugtaks view, extended the buglisting to offer date_last_updated,
3771
    @property
3772
    def can_show_expirable_bugs(self):
3773
        """Return True or False if expirable bug listing can be shown."""
3774
        return target_has_expirable_bugs_listing(self.context)
3775
3776
    @property
3777
    def inactive_expiration_age(self):
3778
        """Return the number of days an bug must be inactive to expire."""
3779
        return config.malone.days_before_expiration
3780
3781
    @property
3782
    def columns_to_show(self):
3783
        """Show the columns that summarise expirable bugs."""
3784
        if (IDistribution.providedBy(self.context)
3785
            or IDistroSeries.providedBy(self.context)):
7675.532.2 by Abel Deuring
upstream report template changed to show the number of bugs having patchs that might be intersting for upstream
3786
            return [
10230.1.3 by Deryck Hodge
Fix lint warnings.
3787
                'id', 'summary', 'packagename', 'date_last_updated', 'heat']
5254.2.11 by Curtis Hovey
Added the expirable bugtaks view, extended the buglisting to offer date_last_updated,
3788
        else:
7675.480.1 by Tom Berger
display the bug heat flames in bug listings
3789
            return ['id', 'summary', 'date_last_updated', 'heat']
5254.2.11 by Curtis Hovey
Added the expirable bugtaks view, extended the buglisting to offer date_last_updated,
3790
5254.2.18 by Curtis Hovey
Consolidated code.
3791
    @property
5254.2.11 by Curtis Hovey
Added the expirable bugtaks view, extended the buglisting to offer date_last_updated,
3792
    def search(self):
3793
        """Return an `ITableBatchNavigator` for the expirable bugtasks."""
11057.8.2 by Brian Murray
modify can_expire to use the days_before_expiration config option
3794
        days_old = config.malone.days_before_expiration
5254.2.18 by Curtis Hovey
Consolidated code.
3795
        bugtaskset = getUtility(IBugTaskSet)
5565.6.3 by Bjorn Tillenius
make the user parameter to findExpirableBugtasks() required. make all callsites specify it.
3796
        bugtasks = bugtaskset.findExpirableBugTasks(
11057.8.2 by Brian Murray
modify can_expire to use the days_before_expiration config option
3797
            days_old, user=self.user, target=self.context)
5254.2.11 by Curtis Hovey
Added the expirable bugtaks view, extended the buglisting to offer date_last_updated,
3798
        return BugListingBatchNavigator(
3799
            bugtasks, self.request, columns_to_show=self.columns_to_show,
3800
            size=config.malone.buglist_batch_size)
8071.6.1 by Graham Binns
Added a basic implementation of BugActivityItem, which works for summaries only.
3801
9292.1.1 by Abel Deuring
LP3 layout for xx-incomplete-bugs.txt
3802
    @property
3803
    def page_title(self):
3804
        return "Bugs that can expire in %s" % self.context.title
3805
8071.6.1 by Graham Binns
Added a basic implementation of BugActivityItem, which works for summaries only.
3806
3807
class BugActivityItem:
3808
    """A decorated BugActivity."""
3809
    delegates(IBugActivity, 'activity')
3810
3811
    def __init__(self, activity):
3812
        self.activity = activity
3813
3814
    @property
3815
    def change_summary(self):
3816
        """Return a formatted summary of the change."""
12366.8.5 by Gary Poster
respond to review by adding docstring and test for get_key (and rename it to get_activity_key); change BugActivity attribute to normalize attribute names and add tests for the behavior; and change get_activity_key to rely on attribute/target rather than whatchanged.
3817
        if self.target is not None:
3818
            # This is a bug task.  We want the attribute, as filtered out.
3819
            return self.attribute
3820
        else:
3821
            # Otherwise, the attribute is more normalized than what we want.
3822
            # Use "whatchanged," which sometimes is more descriptive.
3823
            return self.whatchanged
8071.6.1 by Graham Binns
Added a basic implementation of BugActivityItem, which works for summaries only.
3824
3825
    @property
8071.7.4 by Graham Binns
Added _formatted_tags_change to BugActivityItem.
3826
    def _formatted_tags_change(self):
3827
        """Return a tags change as lists of added and removed tags."""
3828
        assert self.whatchanged == 'tags', (
3829
            "Can't return a formatted tags change for a change in %s."
3830
            % self.whatchanged)
3831
3832
        # Turn the strings of newvalue and oldvalue into sets so we
3833
        # can work out the differences.
3834
        if self.newvalue != '':
8071.8.2 by Graham Binns
Review changes for intellectronica.
3835
            new_tags = set(re.split('\s+', self.newvalue))
8071.7.4 by Graham Binns
Added _formatted_tags_change to BugActivityItem.
3836
        else:
3837
            new_tags = set()
3838
3839
        if self.oldvalue != '':
8071.8.2 by Graham Binns
Review changes for intellectronica.
3840
            old_tags = set(re.split('\s+', self.oldvalue))
8071.7.4 by Graham Binns
Added _formatted_tags_change to BugActivityItem.
3841
        else:
3842
            old_tags = set()
3843
3844
        added_tags = sorted(new_tags.difference(old_tags))
3845
        removed_tags = sorted(old_tags.difference(new_tags))
3846
3847
        return_string = ''
3848
        if len(added_tags) > 0:
3849
            return_string = "added: %s\n" % ' '.join(added_tags)
3850
        if len(removed_tags) > 0:
8071.8.2 by Graham Binns
Review changes for intellectronica.
3851
            return_string = (
3852
                return_string + "removed: %s" % ' '.join(removed_tags))
8071.7.4 by Graham Binns
Added _formatted_tags_change to BugActivityItem.
3853
3854
        # Trim any leading or trailing \ns and then convert the to
3855
        # <br />s so they're displayed correctly.
3856
        return return_string.strip('\n')
3857
3858
    @property
8071.6.1 by Graham Binns
Added a basic implementation of BugActivityItem, which works for summaries only.
3859
    def change_details(self):
3860
        """Return a detailed description of the change."""
8071.9.5 by Graham Binns
Added milestone handling.
3861
        # Our default return dict. We may mutate this depending on
3862
        # what's changed.
3863
        return_dict = {
3864
            'old_value': self.oldvalue,
3865
            'new_value': self.newvalue,
3866
            }
12366.8.5 by Gary Poster
respond to review by adding docstring and test for get_key (and rename it to get_activity_key); change BugActivity attribute to normalize attribute names and add tests for the behavior; and change get_activity_key to rely on attribute/target rather than whatchanged.
3867
        attribute = self.attribute
12366.8.8 by Gary Poster
fix remaining failing tests
3868
        if attribute == 'title':
8071.9.13 by Graham Binns
Description changes are now interleaved as 'description: updated'.
3869
            # We display summary changes as a unified diff, replacing
3870
            # \ns with <br />s so that the lines are separated properly.
8071.6.6 by Graham Binns
Added escaping of newlines to BugActivityItem.
3871
            diff = cgi.escape(
3872
                get_unified_diff(self.oldvalue, self.newvalue, 72), True)
3873
            return diff.replace("\n", "<br />")
8071.9.4 by Graham Binns
Added assignee handling.
3874
12366.8.5 by Gary Poster
respond to review by adding docstring and test for get_key (and rename it to get_activity_key); change BugActivity attribute to normalize attribute names and add tests for the behavior; and change get_activity_key to rely on attribute/target rather than whatchanged.
3875
        elif attribute == 'description':
8071.9.13 by Graham Binns
Description changes are now interleaved as 'description: updated'.
3876
            # Description changes can be quite long, so we just return
3877
            # 'updated' rather than returning the whole new description
3878
            # or a diff.
3879
            return 'updated'
3880
12366.8.5 by Gary Poster
respond to review by adding docstring and test for get_key (and rename it to get_activity_key); change BugActivity attribute to normalize attribute names and add tests for the behavior; and change get_activity_key to rely on attribute/target rather than whatchanged.
3881
        elif attribute == 'tags':
8071.7.4 by Graham Binns
Added _formatted_tags_change to BugActivityItem.
3882
            # We special-case tags because we can work out what's been
3883
            # added and what's been removed.
12792.8.2 by William Grant
Escape all unescaped text in BugActivityItem.change_details. Only assignee was actually exploitable, but the others were close.
3884
            return cgi.escape(self._formatted_tags_change).replace(
3885
                '\n', '<br />')
8071.9.4 by Graham Binns
Added assignee handling.
3886
12366.8.5 by Gary Poster
respond to review by adding docstring and test for get_key (and rename it to get_activity_key); change BugActivity attribute to normalize attribute names and add tests for the behavior; and change get_activity_key to rely on attribute/target rather than whatchanged.
3887
        elif attribute == 'assignee':
8071.9.4 by Graham Binns
Added assignee handling.
3888
            for key in return_dict:
3889
                if return_dict[key] is None:
3890
                    return_dict[key] = 'nobody'
12792.8.2 by William Grant
Escape all unescaped text in BugActivityItem.change_details. Only assignee was actually exploitable, but the others were close.
3891
                else:
3892
                    return_dict[key] = cgi.escape(return_dict[key])
8071.9.4 by Graham Binns
Added assignee handling.
3893
12366.8.5 by Gary Poster
respond to review by adding docstring and test for get_key (and rename it to get_activity_key); change BugActivity attribute to normalize attribute names and add tests for the behavior; and change get_activity_key to rely on attribute/target rather than whatchanged.
3894
        elif attribute == 'milestone':
8071.9.5 by Graham Binns
Added milestone handling.
3895
            for key in return_dict:
3896
                if return_dict[key] is None:
3897
                    return_dict[key] = 'none'
12792.8.2 by William Grant
Escape all unescaped text in BugActivityItem.change_details. Only assignee was actually exploitable, but the others were close.
3898
                else:
3899
                    return_dict[key] = cgi.escape(return_dict[key])
8071.9.4 by Graham Binns
Added assignee handling.
3900
8071.6.2 by Graham Binns
Added implementation for dealing with simple changes to BugActivityItem.
3901
        else:
8071.9.5 by Graham Binns
Added milestone handling.
3902
            # Our default state is to just return oldvalue and newvalue.
3903
            # Since we don't necessarily know what they are, we escape
3904
            # them.
3905
            for key in return_dict:
3906
                return_dict[key] = cgi.escape(return_dict[key])
3907
3908
        return "%(old_value)s &#8594; %(new_value)s" % return_dict
9094.1.3 by Gavin Panella
Add BugTask breadcrumbs.
3909
3910
9209.4.5 by Guilherme Salgado
Update all existing breadcrumb adapters to use Breadcrumb rather than BreadcrumbBuilder. Also rename them all
3911
class BugTaskBreadcrumb(Breadcrumb):
3912
    """Breadcrumb for an `IBugTask`."""
9094.1.15 by Gavin Panella
Don't generate a breadcrumb for a bug that the user is not permitted to see.
3913
9209.4.5 by Guilherme Salgado
Update all existing breadcrumb adapters to use Breadcrumb rather than BreadcrumbBuilder. Also rename them all
3914
    def __init__(self, context):
3915
        super(BugTaskBreadcrumb, self).__init__(context)
3916
        # If the user does not have permission to view the bug for
3917
        # whatever reason, raise ComponentLookupError.
3918
        try:
9570.15.32 by Gavin Panella
Fix lint.
3919
            context.bug.displayname
9209.4.5 by Guilherme Salgado
Update all existing breadcrumb adapters to use Breadcrumb rather than BreadcrumbBuilder. Also rename them all
3920
        except Unauthorized:
3921
            raise ComponentLookupError()
3922
9094.1.3 by Gavin Panella
Add BugTask breadcrumbs.
3923
    @property
3924
    def text(self):
9094.1.5 by Gavin Panella
Just show "Bug #X" as the breadcrumb.
3925
        return self.context.bug.displayname