~launchpad-pqm/launchpad/devel

13779.3.4 by Steve Kowalik
Update copyright years, and fix more test failures for dead things.
1
# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
8687.15.17 by Karl Fogel
Add the copyright header block to the rest of the files under lib/lp/.
2
# GNU Affero General Public License version 3 (see the file LICENSE).
3
8511.2.13 by Aaron Bentley
Fix line wrap
4
# pylint: disable-msg=C0322,F0401
4414.5.13 by Tim Penhey
Still work in progress
5
6
"""Views, navigation and actions for BranchMergeProposals."""
7
8
__metaclass__ = type
9
__all__ = [
6699.2.12 by Paul Hummer
Adds merge candidate views
10
    'BranchMergeCandidateView',
9476.1.3 by Tim Penhey
Breadcrumbs for branches, and fix bmp-index a bit.
11
    'BranchMergeProposalActionNavigationMenu',
7055.6.11 by Tim Penhey
Lots of changes to do with the request reviewer, voting and comment pages.
12
    'BranchMergeProposalAddVoteView',
7055.6.6 by Tim Penhey
Add an edit status view for merge proposals and remove the menu links.
13
    'BranchMergeProposalChangeStatusView',
7954.3.6 by Paul Hummer
Responded to review
14
    'BranchMergeProposalCommitMessageEditView',
4414.5.13 by Tim Penhey
Still work in progress
15
    'BranchMergeProposalContextMenu',
5280.4.24 by Tim Penhey
First cut of changes following review.
16
    'BranchMergeProposalDeleteView',
5579.2.1 by Tim Penhey
Some queue management
17
    'BranchMergeProposalDequeueView',
7675.548.2 by Tim Penhey
Update for the description field.
18
    'BranchMergeProposalDescriptionEditView',
9301.2.43 by Paul Hummer
Re-arranged again, implemented menu
19
    'BranchMergeProposalEditMenu',
4414.5.13 by Tim Penhey
Still work in progress
20
    'BranchMergeProposalEditView',
5579.2.1 by Tim Penhey
Some queue management
21
    'BranchMergeProposalEnqueueView',
5579.2.4 by Tim Penhey
UI works now.
22
    'BranchMergeProposalInlineDequeueView',
23
    'BranchMergeProposalJumpQueueView',
5608.11.5 by Aaron Bentley
Make comment pages reachable
24
    'BranchMergeProposalNavigation',
4414.5.13 by Tim Penhey
Still work in progress
25
    'BranchMergeProposalMergedView',
6998.1.1 by Tim Penhey
Update the primary contexts for branch merge proposals, branch subscriptions, code review comments, and bug branch links.
26
    'BranchMergeProposalPrimaryContext',
5280.4.2 by Tim Penhey
Request review done.
27
    'BranchMergeProposalRequestReviewView',
5600.2.2 by Tim Penhey
Resubmit merge proposals.
28
    'BranchMergeProposalResubmitView',
6699.2.1 by Paul Hummer
Modifies views for some of the subscription stuff
29
    'BranchMergeProposalSubscribersView',
5280.4.8 by Tim Penhey
Added +index view for proposals
30
    'BranchMergeProposalView',
6438.2.3 by Tim Penhey
Add the tables for reviews done, and requested reviews.
31
    'BranchMergeProposalVoteView',
9691.7.9 by Tim Penhey
Extract the logic into a common method and test it.
32
    'latest_proposals_for_each_branch',
4414.5.13 by Tim Penhey
Still work in progress
33
    ]
34
13333.13.40 by Gavin Panella
Use BranchMergeProposalDelta.monitor() in browser code.
35
from functools import wraps
6438.2.3 by Tim Penhey
Add the tables for reviews done, and requested reviews.
36
import operator
37
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
38
from lazr.delegates import delegates
39
from lazr.restful.interface import copy_field
13333.6.43 by Gavin Panella
Revert changes to lp.code; these will appear in a later branch.
40
from lazr.restful.interfaces import (
13302.8.3 by Andrew Bennetts
Generate correct links to fetch diffs from.
41
    IJSONRequestCache,
13333.6.43 by Gavin Panella
Revert changes to lp.code; these will appear in a later branch.
42
    IWebServiceClientRequest,
43
    )
9848.1.5 by Paul Hummer
Got the widget working with the API.
44
import simplejson
7285.2.1 by Paul Hummer
Added class to the comment form in a reply
45
from zope.app.form.browser import TextAreaWidget
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
46
from zope.component import (
47
    adapts,
48
    getMultiAdapter,
49
    getUtility,
50
    )
6080.5.8 by Aaron Bentley
Resubmit passes tests
51
from zope.formlib import form
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
52
from zope.interface import (
53
    implements,
54
    Interface,
55
    )
56
from zope.schema import (
11812.2.3 by Aaron Bentley
Add break link option to resubmit.
57
    Bool,
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
58
    Choice,
59
    Int,
60
    Text,
61
    )
62
from zope.schema.vocabulary import (
63
    SimpleTerm,
64
    SimpleVocabulary,
65
    )
7876.3.6 by Francis J. Lacoste
Used ISQLObject from lazr.lifecycle
66
7675.292.6 by Tim Penhey
Let the users know if the diff has been truncated.
67
from canonical.config import config
14600.1.12 by Curtis Hovey
Move i18n to lp.
68
from lp import _
14600.2.2 by Curtis Hovey
Moved webapp to lp.services.
69
from lp.services.webapp import (
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
70
    canonical_url,
71
    ContextMenu,
72
    enabled_with_permission,
73
    LaunchpadView,
74
    Link,
75
    Navigation,
76
    stepthrough,
77
    stepto,
78
    )
14600.2.2 by Curtis Hovey
Moved webapp to lp.services.
79
from lp.services.webapp.authorization import check_permission
80
from lp.services.webapp.breadcrumb import Breadcrumb
81
from lp.services.webapp.interfaces import IPrimaryContext
82
from lp.services.webapp.menu import (
13333.15.1 by Gavin Panella
Subscribe merge proposal pages to pending diff job completion events.
83
    NavigationMenu,
84
    structured,
85
    )
11929.9.1 by Tim Penhey
Move launchpadform into lp.app.browser.
86
from lp.app.browser.launchpadform import (
87
    action,
88
    custom_widget,
89
    LaunchpadEditFormView,
90
    LaunchpadFormView,
13333.15.1 by Gavin Panella
Subscribe merge proposal pages to pending diff job completion events.
91
    )
12268.3.18 by Tim Penhey
Move the widgets around.
92
from lp.app.browser.lazrjs import (
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
93
    TextAreaEditorWidget,
94
    vocabulary_to_choice_edit_items,
95
    )
12268.3.18 by Tim Penhey
Move the widgets around.
96
from lp.app.browser.tales import DateTimeFormatterAPI
13333.15.1 by Gavin Panella
Subscribe merge proposal pages to pending diff job completion events.
97
from lp.app.longpoll import subscribe
13333.13.54 by Gavin Panella
Rename BranchMergeProposalDelta to BranchMergeProposalNoPreviewDiffDelta and BranchMergeProposalWithPreviewDiffDelta to BranchMergeProposalDelta, and change BranchMergeProposalNoPreviewDiffDelta to inherit from BranchMergeProposalDelta.
98
from lp.code.adapters.branch import BranchMergeProposalNoPreviewDiffDelta
8971.8.1 by Tim Penhey
Make the +reply and +comment views use the service/comment rendering.
99
from lp.code.browser.codereviewcomment import CodeReviewDisplayComment
12505.6.1 by Ian Booth
Remove DecoratedBug and refactor mp linked_bugs to use branch.getRelatedBugTasks
100
from lp.code.browser.decorations import DecoratedBranch
8555.2.5 by Tim Penhey
Move the branch subscription enums.
101
from lp.code.enums import (
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
102
    BranchMergeProposalStatus,
103
    BranchType,
104
    CodeReviewNotificationLevel,
105
    CodeReviewVote,
106
    )
12959.1.3 by Aaron Bentley
Review claim failures generate error notification, not oops.
107
from lp.code.errors import (
13278.2.4 by Aaron Bentley
Handle BranchMergeProposalExists as a user error.
108
    BranchMergeProposalExists,
12959.1.3 by Aaron Bentley
Review claim failures generate error notification, not oops.
109
    ClaimReviewFailed,
13618.3.1 by Aaron Bentley
InvalidMergeProposal is handled as a user error.
110
    InvalidBranchMergeProposal,
12959.1.3 by Aaron Bentley
Review claim failures generate error notification, not oops.
111
    WrongBranchMergeProposal,
112
    )
7675.429.3 by Tim Penhey
Move merge proposal errors to lp.code.errors.
113
from lp.code.interfaces.branchmergeproposal import IBranchMergeProposal
8555.2.9 by Tim Penhey
Move CodeReviewVote enum.
114
from lp.code.interfaces.codereviewcomment import ICodeReviewComment
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
115
from lp.code.interfaces.codereviewvote import ICodeReviewVoteReference
9691.6.10 by Tim Penhey
Make the diff load using the API rather than hitting the page.
116
from lp.code.interfaces.diff import IPreviewDiff
7675.293.10 by Aaron Bentley
Remove unused import.
117
from lp.registry.interfaces.person import IPersonSet
9984.4.3 by Tim Penhey
Broken tests are there...
118
from lp.services.comments.interfaces.conversation import (
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
119
    IComment,
120
    IConversation,
121
    )
7675.916.98 by Henning Eggers
Merged db-stable at r10026 (recife roll-back) but without accepting the changes.
122
from lp.services.features import getFeatureFlag
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
123
from lp.services.fields import (
124
    Summary,
125
    Whiteboard,
126
    )
13333.15.1 by Gavin Panella
Subscribe merge proposal pages to pending diff job completion events.
127
from lp.services.messages.interfaces.message import IMessageSet
11382.6.34 by Gavin Panella
Reformat imports in all files touched so far.
128
from lp.services.propertycache import cachedproperty
6438.2.1 by Tim Penhey
Renamed the CodeReviewVote class to avoid name clash with CodeReviewVote enumerated type.
129
4414.5.13 by Tim Penhey
Still work in progress
130
9691.7.9 by Tim Penhey
Extract the logic into a common method and test it.
131
def latest_proposals_for_each_branch(proposals):
132
    """Returns the most recent merge proposals for any particular branch.
133
134
    Also filters out proposals that the logged in user can't see.
135
    """
136
    targets = {}
137
    for proposal in proposals:
138
        # Don't show the proposal if the user can't see it.
139
        if not check_permission('launchpad.View', proposal):
140
            continue
141
        # Only show the must recent proposal for any given target.
142
        date_created = proposal.date_created
143
        target_id = proposal.target_branch.id
9691.7.28 by Tim Penhey
Tweak latest_proposals_for_each_branch as per review suggestion.
144
145
        if target_id not in targets or date_created > targets[target_id][1]:
9691.7.9 by Tim Penhey
Extract the logic into a common method and test it.
146
            targets[target_id] = (proposal, date_created)
147
148
    return sorted(
149
        [proposal for proposal, date_created in targets.itervalues()],
150
        key=operator.attrgetter('date_created'), reverse=True)
151
152
6998.1.1 by Tim Penhey
Update the primary contexts for branch merge proposals, branch subscriptions, code review comments, and bug branch links.
153
class BranchMergeProposalPrimaryContext:
154
    """The primary context is the proposal is that of the source branch."""
155
156
    implements(IPrimaryContext)
157
158
    def __init__(self, branch_merge_proposal):
159
        self.context = IPrimaryContext(
160
            branch_merge_proposal.source_branch).context
161
162
9476.1.4 by Tim Penhey
Give merge proposals a breadcrumb, and some minor drive by fixes.
163
class BranchMergeProposalBreadcrumb(Breadcrumb):
164
    """An `IBreadcrumb` for a merge proposal."""
165
166
    @property
167
    def text(self):
168
        return 'Merge into %s' % self.context.target_branch.name
169
170
6080.6.1 by Aaron Bentley
Refactor notify decorator out of update_and_notify
171
def notify(func):
6080.6.3 by Aaron Bentley
Add final punctuation to docstrings
172
    """Decorate a view method to send a notification."""
13333.13.40 by Gavin Panella
Use BranchMergeProposalDelta.monitor() in browser code.
173
    @wraps(func)
6080.6.2 by Aaron Bentley
Tweak and update docs
174
    def decorator(view, *args, **kwargs):
13333.13.54 by Gavin Panella
Rename BranchMergeProposalDelta to BranchMergeProposalNoPreviewDiffDelta and BranchMergeProposalWithPreviewDiffDelta to BranchMergeProposalDelta, and change BranchMergeProposalNoPreviewDiffDelta to inherit from BranchMergeProposalDelta.
175
        with BranchMergeProposalNoPreviewDiffDelta.monitor(view.context):
13333.13.40 by Gavin Panella
Use BranchMergeProposalDelta.monitor() in browser code.
176
            return func(view, *args, **kwargs)
6080.6.1 by Aaron Bentley
Refactor notify decorator out of update_and_notify
177
    return decorator
178
179
180
def update_and_notify(func):
6080.6.3 by Aaron Bentley
Add final punctuation to docstrings
181
    """Decorate an action to update from a form and send a notification."""
11626.3.10 by Curtis Hovey
Hush lints epic complaints about the changes files.
182
6080.6.1 by Aaron Bentley
Refactor notify decorator out of update_and_notify
183
    @notify
184
    def decorator(view, action, data):
185
        result = func(view, action, data)
6080.5.8 by Aaron Bentley
Resubmit passes tests
186
        form.applyChanges(
187
            view.context, view.form_fields, data, view.adapters)
188
        return result
189
    return decorator
190
191
6699.2.12 by Paul Hummer
Adds merge candidate views
192
class BranchMergeCandidateView(LaunchpadView):
193
    """Provides a small fragment of landing targets"""
194
195
    def friendly_text(self):
196
        """Prints friendly text for a branch."""
197
        friendly_texts = {
11626.3.10 by Curtis Hovey
Hush lints epic complaints about the changes files.
198
            BranchMergeProposalStatus.WORK_IN_PROGRESS: 'On hold',
199
            BranchMergeProposalStatus.NEEDS_REVIEW: 'Ready for review',
200
            BranchMergeProposalStatus.CODE_APPROVED: 'Approved',
201
            BranchMergeProposalStatus.REJECTED: 'Rejected',
202
            BranchMergeProposalStatus.MERGED: 'Merged',
203
            BranchMergeProposalStatus.MERGE_FAILED:
6735.1.1 by Paul Hummer
Adds merge proposal stuffs
204
                'Approved [Merge Failed]',
11626.3.10 by Curtis Hovey
Hush lints epic complaints about the changes files.
205
            BranchMergeProposalStatus.QUEUED: 'Queued',
206
            BranchMergeProposalStatus.SUPERSEDED: 'Superseded',
11486.5.14 by Aaron Bentley
Get incremental diffs displaying
207
        }
6699.2.12 by Paul Hummer
Adds merge candidate views
208
        return friendly_texts[self.context.queue_status]
209
9691.5.5 by Tim Penhey
Confirm that the title is set properly.
210
    @property
9691.4.1 by Tim Penhey
Add title to the link summary, move the link summary to have a consistent file name, add the vote summary view, template and show it on the branch page.
211
    def status_title(self):
212
        """The title for the status text.
213
214
        Only set if the status is approved or rejected.
215
        """
216
        result = ''
217
        if self.context.queue_status in (
218
            BranchMergeProposalStatus.CODE_APPROVED,
11486.5.14 by Aaron Bentley
Get incremental diffs displaying
219
            BranchMergeProposalStatus.REJECTED):
9691.4.1 by Tim Penhey
Add title to the link summary, move the link summary to have a consistent file name, add the vote summary view, template and show it on the branch page.
220
            formatter = DateTimeFormatterAPI(self.context.date_reviewed)
221
            result = '%s %s' % (
222
                self.context.reviewer.displayname,
223
                formatter.displaydate())
224
        return result
225
6699.2.12 by Paul Hummer
Adds merge candidate views
226
9476.1.3 by Tim Penhey
Breadcrumbs for branches, and fix bmp-index a bit.
227
class BranchMergeProposalMenuMixin:
228
    """Mixin class for merge proposal menus."""
6438.3.24 by Aaron Bentley
Updates for LP2.0
229
230
    @enabled_with_permission('launchpad.AnyPerson')
231
    def add_comment(self):
9461.9.1 by Aaron Bentley
Allow voting and reviewing when proposal isn't mergeable.
232
        return Link('+comment', 'Add a review or comment', icon='add')
5280.4.11 by Tim Penhey
Read only page is now the default view, edit pages hang off that.
233
234
    @enabled_with_permission('launchpad.Edit')
235
    def edit(self):
236
        text = 'Edit details'
7055.6.17 by Tim Penhey
Fixed resubmit and link visibility.
237
        enabled = self.context.isMergable()
5600.2.7 by Tim Penhey
Updated pagetest for resubmissions.
238
        return Link('+edit', text, icon='edit', enabled=enabled)
5280.4.8 by Tim Penhey
Added +index view for proposals
239
240
    @enabled_with_permission('launchpad.Edit')
7675.548.2 by Tim Penhey
Update for the description field.
241
    def set_description(self):
242
        text = 'Set description'
243
        return Link('+edit-description', text, icon='add')
244
245
    @enabled_with_permission('launchpad.Edit')
9928.2.1 by Tim Penhey
First hack.
246
    def set_commit_message(self):
247
        text = 'Set commit message'
7954.3.3 by Paul Hummer
Added link to the commit message edit view
248
        enabled = self.context.isMergable()
9928.2.3 by Tim Penhey
Trying... but it seems that simulate is 3.0 release.
249
        return Link('+edit-commit-message', text, icon='add',
8511.2.13 by Aaron Bentley
Fix line wrap
250
                    enabled=enabled)
7954.3.3 by Paul Hummer
Added link to the commit message edit view
251
252
    @enabled_with_permission('launchpad.Edit')
7055.6.6 by Tim Penhey
Add an edit status view for merge proposals and remove the menu links.
253
    def edit_status(self):
254
        text = 'Change status'
8511.2.12 by Aaron Bentley
Allow status changes for merged or superseded BMP
255
        return Link('+edit-status', text, icon='edit')
7055.6.6 by Tim Penhey
Add an edit status view for merge proposals and remove the menu links.
256
257
    @enabled_with_permission('launchpad.Edit')
5280.4.24 by Tim Penhey
First cut of changes following review.
258
    def delete(self):
259
        text = 'Delete proposal to merge'
9476.1.3 by Tim Penhey
Breadcrumbs for branches, and fix bmp-index a bit.
260
        return Link('+delete', text, icon='trash-icon')
5280.4.24 by Tim Penhey
First cut of changes following review.
261
5579.2.4 by Tim Penhey
UI works now.
262
    def _enabledForStatus(self, next_state):
263
        """True if the next_state is a valid transition for the current user.
264
265
        Return False if the current state is next_state.
266
        """
9476.1.3 by Tim Penhey
Breadcrumbs for branches, and fix bmp-index a bit.
267
        bmp = self.branch_merge_proposal
268
        status = bmp.queue_status
5579.2.4 by Tim Penhey
UI works now.
269
        if status == next_state:
270
            return False
271
        else:
9476.1.3 by Tim Penhey
Breadcrumbs for branches, and fix bmp-index a bit.
272
            return bmp.isValidTransition(next_state, self.user)
5579.2.4 by Tim Penhey
UI works now.
273
5280.4.24 by Tim Penhey
First cut of changes following review.
274
    @enabled_with_permission('launchpad.Edit')
5280.4.1 by Tim Penhey
Adding menu bits.
275
    def request_review(self):
6602.1.1 by Tim Penhey
Some touchy feely changes to code review page.!
276
        text = 'Request a review'
5579.2.4 by Tim Penhey
UI works now.
277
        enabled = self._enabledForStatus(
278
            BranchMergeProposalStatus.NEEDS_REVIEW)
6438.3.9 by Aaron Bentley
Support requesting additional reviewers
279
        if (self.context.queue_status ==
280
            BranchMergeProposalStatus.NEEDS_REVIEW):
281
            enabled = True
6438.3.23 by Aaron Bentley
Update from review
282
            if (self.context.votes.count()) > 0:
6438.3.9 by Aaron Bentley
Support requesting additional reviewers
283
                text = 'Request another review'
6735.1.1 by Paul Hummer
Adds merge proposal stuffs
284
        return Link('+request-review', text, icon='add', enabled=enabled)
5280.4.1 by Tim Penhey
Adding menu bits.
285
286
    @enabled_with_permission('launchpad.Edit')
4414.5.13 by Tim Penhey
Still work in progress
287
    def merge(self):
288
        text = 'Mark as merged'
5579.2.4 by Tim Penhey
UI works now.
289
        enabled = self._enabledForStatus(
290
            BranchMergeProposalStatus.MERGED)
6735.1.1 by Paul Hummer
Adds merge proposal stuffs
291
        return Link('+merged', text, enabled=enabled)
4414.5.13 by Tim Penhey
Still work in progress
292
5579.2.1 by Tim Penhey
Some queue management
293
    @enabled_with_permission('launchpad.Edit')
6914.1.10 by Paul Hummer
For some boneheaded reason, I did originally make that edit of merge revision a
294
    def update_merge_revno(self):
295
        text = 'Update revision number'
296
        return Link('+merged', text)
297
298
    @enabled_with_permission('launchpad.Edit')
5579.2.1 by Tim Penhey
Some queue management
299
    def enqueue(self):
300
        text = 'Queue for merging'
5579.2.4 by Tim Penhey
UI works now.
301
        enabled = self._enabledForStatus(
302
            BranchMergeProposalStatus.QUEUED)
6735.1.1 by Paul Hummer
Adds merge proposal stuffs
303
        return Link('+enqueue', text, enabled=enabled)
5579.2.1 by Tim Penhey
Some queue management
304
5579.2.3 by Tim Penhey
Merged dependent branch.
305
    @enabled_with_permission('launchpad.Edit')
5579.2.4 by Tim Penhey
UI works now.
306
    def dequeue(self):
307
        text = 'Remove from queue'
308
        enabled = (self.context.queue_status ==
309
                   BranchMergeProposalStatus.QUEUED)
6735.1.1 by Paul Hummer
Adds merge proposal stuffs
310
        return Link('+dequeue', text, enabled=enabled)
5579.2.4 by Tim Penhey
UI works now.
311
312
    @enabled_with_permission('launchpad.Edit')
5600.2.2 by Tim Penhey
Resubmit merge proposals.
313
    def resubmit(self):
314
        text = 'Resubmit proposal'
5579.2.4 by Tim Penhey
UI works now.
315
        enabled = self._enabledForStatus(
5600.2.10 by Tim Penhey
Rename supercede to supersede, and shorten the databse field name.
316
            BranchMergeProposalStatus.SUPERSEDED)
9327.2.4 by Aaron Bentley
Use edit icon.
317
        return Link('+resubmit', text, enabled=enabled, icon='edit')
5600.2.2 by Tim Penhey
Resubmit merge proposals.
318
4414.5.13 by Tim Penhey
Still work in progress
319
9301.2.43 by Paul Hummer
Re-arranged again, implemented menu
320
class BranchMergeProposalEditMenu(NavigationMenu,
321
                                  BranchMergeProposalMenuMixin):
322
    """Edit menu for Branch Merge Proposals."""
323
324
    usedfor = IBranchMergeProposal
325
    title = 'Edit Proposal'
326
    facet = 'branches'
9570.13.3 by Paul Hummer
General housekeeping
327
    links = ['resubmit', 'delete']
9301.2.43 by Paul Hummer
Re-arranged again, implemented menu
328
329
    @property
330
    def branch_merge_proposal(self):
331
        return self.context
332
333
9476.1.3 by Tim Penhey
Breadcrumbs for branches, and fix bmp-index a bit.
334
class BranchMergeProposalContextMenu(ContextMenu,
335
                                     BranchMergeProposalMenuMixin):
336
    """Context menu for merge proposals."""
337
338
    usedfor = IBranchMergeProposal
339
    links = [
340
        'add_comment',
9570.13.3 by Paul Hummer
General housekeeping
341
        'dequeue',
9928.2.1 by Tim Penhey
First hack.
342
        'set_commit_message',
7675.548.2 by Tim Penhey
Update for the description field.
343
        'set_description',
9570.13.3 by Paul Hummer
General housekeeping
344
        'edit_status',
345
        'enqueue',
9476.1.3 by Tim Penhey
Breadcrumbs for branches, and fix bmp-index a bit.
346
        'merge',
9570.13.3 by Paul Hummer
General housekeeping
347
        'request_review',
9476.1.3 by Tim Penhey
Breadcrumbs for branches, and fix bmp-index a bit.
348
        'resubmit',
349
        'update_merge_revno',
350
        ]
351
352
    @property
353
    def branch_merge_proposal(self):
354
        return self.context
355
356
357
class IBranchMergeProposalActionMenu(Interface):
358
    """A marker interface for the global action navigation menu."""
359
360
361
class BranchMergeProposalActionNavigationMenu(NavigationMenu,
362
                                              BranchMergeProposalMenuMixin):
363
    """A sub-menu for acting upon a Product."""
364
365
    usedfor = IBranchMergeProposalActionMenu
366
    facet = 'branches'
367
    links = ('resubmit', 'delete')
368
369
    @property
370
    def branch_merge_proposal(self):
371
        # This context is the view, the view's context is the bmp.
372
        return self.context.context
373
374
5280.4.8 by Tim Penhey
Added +index view for proposals
375
class UnmergedRevisionsMixin:
376
    """Provides the methods needed to show unmerged revisions."""
377
378
    @cachedproperty
379
    def unlanded_revisions(self):
380
        """Return the unlanded revisions from the source branch."""
381
        return self.context.getUnlandedSourceBranchRevisions()
382
383
    @property
8128.7.7 by Jonathan Lange
Make the template use the view for obtaining pending-writes, since the
384
    def pending_writes(self):
385
        """Needed to make the branch-revisions metal macro work."""
386
        return False
387
388
    @property
5280.4.8 by Tim Penhey
Added +index view for proposals
389
    def codebrowse_url(self):
390
        """Return the link to codebrowse for this branch."""
7622.1.11 by Michael Hudson
boy i wish this mistake had turned up some other way than an obscure failure in a page test :/
391
        return self.context.source_branch.codebrowse_url()
5280.4.8 by Tim Penhey
Added +index view for proposals
392
393
5579.2.4 by Tim Penhey
UI works now.
394
class BranchMergeProposalRevisionIdMixin:
395
    """A mixin class to provide access to the revision ids."""
5579.2.1 by Tim Penhey
Some queue management
396
397
    def _getRevisionNumberForRevisionId(self, revision_id):
398
        """Find the revision number that corresponds to the revision id.
5763.1.6 by Tim Penhey
Some view refactoring.
399
400
        If there was no last reviewed revision, None is returned.
401
402
        If the reviewed revision is no longer in the revision history of
403
        the source branch, then a message is returned.
404
        """
5579.2.1 by Tim Penhey
Some queue management
405
        if revision_id is None:
406
            return None
5763.1.6 by Tim Penhey
Some view refactoring.
407
        # If the source branch is REMOTE, then there won't be any ids.
408
        source_branch = self.context.source_branch
409
        if source_branch.branch_type == BranchType.REMOTE:
5579.2.1 by Tim Penhey
Some queue management
410
            return revision_id
5763.1.6 by Tim Penhey
Some view refactoring.
411
        else:
6736.3.5 by Tim Penhey
Clean up the Branch.getBranchRevision method.
412
            branch_revision = source_branch.getBranchRevision(
413
                revision_id=revision_id)
5763.1.6 by Tim Penhey
Some view refactoring.
414
            if branch_revision is None:
415
                return "no longer in the source branch."
416
            elif branch_revision.sequence is None:
417
                return (
418
                    "no longer in the revision history of the source branch.")
419
            else:
420
                return branch_revision.sequence
421
5579.2.1 by Tim Penhey
Some queue management
422
    @cachedproperty
423
    def reviewed_revision_number(self):
424
        """Return the number of the reviewed revision."""
425
        return self._getRevisionNumberForRevisionId(
426
            self.context.reviewed_revision_id)
427
428
    @cachedproperty
429
    def queued_revision_number(self):
430
        """Return the number of the queued revision."""
431
        return self._getRevisionNumberForRevisionId(
432
            self.context.queued_revision_id)
5763.1.6 by Tim Penhey
Some view refactoring.
433
434
5608.11.5 by Aaron Bentley
Make comment pages reachable
435
class BranchMergeProposalNavigation(Navigation):
5608.13.23 by Aaron Bentley
update docs
436
    """Navigation from BranchMergeProposal to CodeReviewComment views."""
5608.11.5 by Aaron Bentley
Make comment pages reachable
437
438
    usedfor = IBranchMergeProposal
439
7675.293.8 by Aaron Bentley
Allow reassigning code review.
440
    @stepthrough('reviews')
441
    def traverse_review(self, id):
7675.293.19 by Aaron Bentley
Cleanup
442
        """Navigate to a CodeReviewVoteReference through its BMP."""
7675.293.8 by Aaron Bentley
Allow reassigning code review.
443
        try:
444
            id = int(id)
445
        except ValueError:
446
            return None
447
        try:
448
            return self.context.getVoteReference(id)
449
        except WrongBranchMergeProposal:
450
            return None
451
5608.11.7 by Aaron Bentley
Update code review comment traversal to match canonical_url
452
    @stepthrough('comments')
5608.11.5 by Aaron Bentley
Make comment pages reachable
453
    def traverse_comment(self, id):
454
        try:
455
            id = int(id)
456
        except ValueError:
457
            return None
5608.11.61 by Aaron Bentley
Handle BranchMergeProposal.getMessage when merge proposal doesn't match
458
        try:
6334.6.36 by Aaron Bentley
Rename code review messages to code review comments
459
            return self.context.getComment(id)
5608.11.61 by Aaron Bentley
Handle BranchMergeProposal.getMessage when merge proposal doesn't match
460
        except WrongBranchMergeProposal:
5608.11.59 by Aaron Bentley
Apply many review comments
461
            return None
5608.11.5 by Aaron Bentley
Make comment pages reachable
462
7667.9.6 by Tim Penhey
Add traversal and tests.
463
    @stepto("+preview-diff")
464
    def preview_diff(self):
465
        """Step to the preview diff."""
466
        return self.context.preview_diff
467
7719.2.15 by Paul Hummer
Implemented getVote for IBranchMergeProposal
468
    @stepthrough('+review')
469
    def review(self, id):
470
        """Step to the CodeReviewVoteReference."""
471
        try:
7719.2.16 by Paul Hummer
Implemented IBranchMergeProposal.id
472
            id = int(id)
7719.2.15 by Paul Hummer
Implemented getVote for IBranchMergeProposal
473
        except ValueError:
474
            return None
475
        try:
476
            return self.context.getVoteReference(id)
477
        except WrongBranchMergeProposal:
478
            return None
5608.11.5 by Aaron Bentley
Make comment pages reachable
479
8624.1.3 by Tim Penhey
Working but needs clean up.
480
481
class CodeReviewConversation:
482
    """A code review conversation."""
483
484
    implements(IConversation)
485
486
    def __init__(self, comments):
487
        self.comments = comments
488
9476.1.3 by Tim Penhey
Breadcrumbs for branches, and fix bmp-index a bit.
489
7675.293.1 by Aaron Bentley
Start work on review claiming.
490
class ClaimButton(Interface):
491
    """A simple interface to populate the form to enqueue a proposal."""
492
7675.293.4 by Aaron Bentley
Start testing claim_action.
493
    review_id = Int(required=True)
7675.293.1 by Aaron Bentley
Start work on review claiming.
494
495
9848.1.13 by Paul Hummer
Fixed the status vocabulary to be generated based on what's enabled.
496
class BranchMergeProposalStatusMixin:
497
    '''A mixin for generating status vocabularies.'''
498
499
    def _createStatusVocabulary(self):
500
        # Create the vocabulary that is used for the status widget.
501
        possible_next_states = (
502
            BranchMergeProposalStatus.WORK_IN_PROGRESS,
503
            BranchMergeProposalStatus.NEEDS_REVIEW,
504
            BranchMergeProposalStatus.CODE_APPROVED,
505
            BranchMergeProposalStatus.REJECTED,
506
            # BranchMergeProposalStatus.QUEUED,
507
            BranchMergeProposalStatus.MERGED,
508
            )
509
        terms = []
510
        for status in possible_next_states:
511
            if not self.context.isValidTransition(status, self.user):
512
                continue
513
            else:
514
                title = status.title
515
            terms.append(SimpleTerm(status, status.name, title))
516
        return SimpleVocabulary(terms)
517
518
9691.6.10 by Tim Penhey
Make the diff load using the API rather than hitting the page.
519
class DiffRenderingMixin:
520
    """A mixin class for handling diff text."""
521
522
    @cachedproperty
523
    def preview_diff_text(self):
524
        """Return a (hopefully) intelligently encoded review diff."""
525
        preview_diff = self.preview_diff
526
        if preview_diff is None:
527
            return None
528
        try:
529
            diff = preview_diff.text.decode('utf-8')
530
        except UnicodeDecodeError:
531
            diff = preview_diff.text.decode('windows-1252', 'replace')
532
        # Strip off the trailing carriage returns.
533
        return diff.rstrip('\n')
534
535
    @cachedproperty
536
    def diff_oversized(self):
13779.3.1 by Steve Kowalik
Consign StaticDiff and all that supported it to a watery grave.
537
        """Return True if the preview_diff is over the configured size limit.
9691.6.10 by Tim Penhey
Make the diff load using the API rather than hitting the page.
538
539
        The diff can be over the limit in two ways.  If the diff is oversized
540
        in bytes it will be cut off at the Diff.text method.  If the number of
541
        lines is over the max_format_lines, then it is cut off at the fmt:diff
542
        processing.
543
        """
13779.3.1 by Steve Kowalik
Consign StaticDiff and all that supported it to a watery grave.
544
        preview_diff = self.preview_diff
545
        if preview_diff is None:
9691.6.10 by Tim Penhey
Make the diff load using the API rather than hitting the page.
546
            return False
547
        diff_text = self.preview_diff_text
548
        return diff_text.count('\n') >= config.diff.max_format_lines
549
9848.1.13 by Paul Hummer
Fixed the status vocabulary to be generated based on what's enabled.
550
7675.831.1 by Michael Nelson
Fixed failing code tests that were rendering the default comment template.
551
class ICodeReviewNewRevisions(IComment):
9984.4.11 by Tim Penhey
Include the revisions in the conversation.
552
    """Marker interface used to register views for CodeReviewNewRevisions."""
553
554
9984.4.3 by Tim Penhey
Broken tests are there...
555
class CodeReviewNewRevisions:
556
    """Represents a logical grouping of revisions.
557
558
    Each object instance represents a number of revisions scanned at a
559
    particular time.
560
    """
7675.831.1 by Michael Nelson
Fixed failing code tests that were rendering the default comment template.
561
    implements(ICodeReviewNewRevisions)
9984.4.3 by Tim Penhey
Broken tests are there...
562
11486.5.37 by Aaron Bentley
Show incremental diffs as part of revision list.
563
    def __init__(self, revisions, date, branch, diff):
9984.4.3 by Tim Penhey
Broken tests are there...
564
        self.revisions = revisions
9984.4.9 by Tim Penhey
Test for groups.
565
        self.branch = branch
9984.4.3 by Tim Penhey
Broken tests are there...
566
        self.has_body = False
567
        self.has_footer = True
568
        # The date attribute is used to sort the comments in the conversation.
569
        self.date = date
11486.5.37 by Aaron Bentley
Show incremental diffs as part of revision list.
570
        self.diff = diff
9984.4.3 by Tim Penhey
Broken tests are there...
571
7675.831.1 by Michael Nelson
Fixed failing code tests that were rendering the default comment template.
572
        # Other standard IComment attributes are not used.
573
        self.extra_css_class = None
574
        self.comment_author = None
575
        self.body_text = None
576
        self.comment_date = None
577
        self.display_attachments = False
578
9984.4.3 by Tim Penhey
Broken tests are there...
579
9984.4.11 by Tim Penhey
Include the revisions in the conversation.
580
class CodeReviewNewRevisionsView(LaunchpadView):
581
    """The view for rendering the new revisions."""
582
583
    @property
584
    def codebrowse_url(self):
585
        """Return the link to codebrowse for this branch."""
586
        return self.context.branch.codebrowse_url()
587
588
7675.293.1 by Aaron Bentley
Start work on review claiming.
589
class BranchMergeProposalView(LaunchpadFormView, UnmergedRevisionsMixin,
9848.1.13 by Paul Hummer
Fixed the status vocabulary to be generated based on what's enabled.
590
                              BranchMergeProposalRevisionIdMixin,
9691.6.10 by Tim Penhey
Make the diff load using the API rather than hitting the page.
591
                              BranchMergeProposalStatusMixin,
592
                              DiffRenderingMixin):
5579.2.4 by Tim Penhey
UI works now.
593
    """A basic view used for the index page."""
594
9476.1.3 by Tim Penhey
Breadcrumbs for branches, and fix bmp-index a bit.
595
    implements(IBranchMergeProposalActionMenu)
596
7675.293.1 by Aaron Bentley
Start work on review claiming.
597
    schema = ClaimButton
598
13302.8.3 by Andrew Bennetts
Generate correct links to fetch diffs from.
599
    def initialize(self):
600
        super(BranchMergeProposalView, self).initialize()
601
        cache = IJSONRequestCache(self.request)
602
        cache.objects.update({
603
            'branch_diff_link':
13333.15.33 by Gavin Panella
Subscribe to the BranchMergeProposal rather than the preview diff job.
604
                'https://%s/+loggerhead/%s/diff/' % (
605
                    config.launchpad.code_domain,
606
                    self.context.source_branch.unique_name),
13302.8.3 by Andrew Bennetts
Generate correct links to fetch diffs from.
607
            })
13333.15.48 by Gavin Panella
Only subscribe the page when the longpoll.merge_proposals.enabled feature flag is enabled.
608
        if getFeatureFlag("longpoll.merge_proposals.enabled"):
13333.15.50 by Gavin Panella
Switch from new_mp_diff_event to merge_proposal_event_key.
609
            cache.objects['merge_proposal_event_key'] = (
610
                subscribe(self.context).event_key)
13302.8.3 by Andrew Bennetts
Generate correct links to fetch diffs from.
611
7675.429.19 by Tim Penhey
Update the story for reassignment.
612
    @action('Claim', name='claim')
7675.293.1 by Aaron Bentley
Start work on review claiming.
613
    def claim_action(self, action, data):
7675.293.4 by Aaron Bentley
Start testing claim_action.
614
        """Claim this proposal."""
7675.293.1 by Aaron Bentley
Start work on review claiming.
615
        request = self.context.getVoteReference(data['review_id'])
7675.429.9 by Tim Penhey
Use the new claimReview method to actually get the review.
616
        if request is not None:
12959.1.3 by Aaron Bentley
Review claim failures generate error notification, not oops.
617
            try:
618
                request.claimReview(self.user)
619
            except ClaimReviewFailed as e:
620
                self.request.response.addErrorNotification(unicode(e))
7675.293.1 by Aaron Bentley
Start work on review claiming.
621
        self.next_url = canonical_url(self.context)
5579.2.4 by Tim Penhey
UI works now.
622
623
    @property
5608.11.26 by Aaron Bentley
Add page and link for creating CodeReviewMessage
624
    def comment_location(self):
5608.15.9 by Aaron Bentley
Update docs
625
        """Location of page for commenting on this proposal."""
5608.11.26 by Aaron Bentley
Add page and link for creating CodeReviewMessage
626
        return canonical_url(self.context, view_name='+comment')
627
8624.1.3 by Tim Penhey
Working but needs clean up.
628
    @cachedproperty
629
    def conversation(self):
630
        """Return a conversation that is to be rendered."""
631
        # Sort the comments by date order.
10272.1.1 by Aaron Bentley
Include comments from superseded proposals.
632
        merge_proposal = self.context
11486.5.42 by Aaron Bentley
Optimize diff lookup.
633
        groups = list(merge_proposal.getRevisionsSinceReviewStart())
11486.2.7 by Aaron Bentley
Move revision selection to model code.
634
        source = DecoratedBranch(merge_proposal.source_branch)
11486.5.37 by Aaron Bentley
Show incremental diffs as part of revision list.
635
        comments = []
11486.5.42 by Aaron Bentley
Optimize diff lookup.
636
        if getFeatureFlag('code.incremental_diffs.enabled'):
637
            ranges = [
638
                (revisions[0].revision.getLefthandParent(),
639
                 revisions[-1].revision)
640
                for revisions in groups]
641
            diffs = merge_proposal.getIncrementalDiffs(ranges)
642
        else:
643
            diffs = [None] * len(groups)
644
        for revisions, diff in zip(groups, diffs):
11486.5.38 by Aaron Bentley
Clean up old approach.
645
            newrevs = CodeReviewNewRevisions(
646
                revisions, revisions[-1].revision.date_created, source, diff)
11486.5.37 by Aaron Bentley
Show incremental diffs as part of revision list.
647
            comments.append(newrevs)
10272.1.1 by Aaron Bentley
Include comments from superseded proposals.
648
        while merge_proposal is not None:
10272.1.2 by Aaron Bentley
Distinguish betwen comments on this proposal and superseded ones.
649
            from_superseded = merge_proposal != self.context
10272.1.1 by Aaron Bentley
Include comments from superseded proposals.
650
            comments.extend(
10272.1.2 by Aaron Bentley
Distinguish betwen comments on this proposal and superseded ones.
651
                CodeReviewDisplayComment(comment, from_superseded)
10272.1.1 by Aaron Bentley
Include comments from superseded proposals.
652
                for comment in merge_proposal.all_comments)
653
            merge_proposal = merge_proposal.supersedes
8624.1.3 by Tim Penhey
Working but needs clean up.
654
        comments = sorted(comments, key=operator.attrgetter('date'))
655
        return CodeReviewConversation(comments)
656
5608.11.41 by Aaron Bentley
Add nearly-threaded display of message subjects
657
    @property
658
    def comments(self):
5608.11.51 by Aaron Bentley
Cleanup
659
        """Return comments associated with this proposal, plus styling info.
660
661
        Comments are in threaded order, and the style indicates indenting
662
        for use with threads.
663
        """
5608.11.41 by Aaron Bentley
Add nearly-threaded display of message subjects
664
        message_to_comment = {}
665
        messages = []
6334.6.36 by Aaron Bentley
Rename code review messages to code review comments
666
        for comment in self.context.all_comments:
5608.11.41 by Aaron Bentley
Add nearly-threaded display of message subjects
667
            message_to_comment[comment.message] = comment
668
            messages.append(comment.message)
5608.11.53 by Aaron Bentley
Move message-threading functionality onto MessageSet
669
        message_set = getUtility(IMessageSet)
670
        threads = message_set.threadMessages(messages)
5608.11.41 by Aaron Bentley
Add nearly-threaded display of message subjects
671
        result = []
5608.11.53 by Aaron Bentley
Move message-threading functionality onto MessageSet
672
        for depth, message in message_set.flattenThreads(threads):
5608.11.41 by Aaron Bentley
Add nearly-threaded display of message subjects
673
            comment = message_to_comment[message]
5608.11.42 by Aaron Bentley
Get actual threading working
674
            style = 'margin-left: %dem;' % (2 * depth)
5608.11.41 by Aaron Bentley
Add nearly-threaded display of message subjects
675
            result.append(dict(style=style, comment=comment))
676
        return result
677
10054.23.1 by Aaron Bentley
Merged stable into jobstatus.
678
    @property
14412.3.6 by mbp at canonical
Better page label on merge proposals
679
    def label(self):
680
        return "Merge %s into %s" % (
681
            self.context.source_branch.bzr_identity,
682
            self.context.target_branch.bzr_identity)
683
684
    @property
10054.23.1 by Aaron Bentley
Merged stable into jobstatus.
685
    def pending_diff(self):
12112.2.1 by Aaron Bentley
Merge proposal diff update message shown when source updated.
686
        return (
13333.15.33 by Gavin Panella
Subscribe to the BranchMergeProposal rather than the preview diff job.
687
            self.context.next_preview_diff_job is not None or
12112.2.1 by Aaron Bentley
Merge proposal diff update message shown when source updated.
688
            self.context.source_branch.pending_writes)
10054.23.1 by Aaron Bentley
Merged stable into jobstatus.
689
7675.292.6 by Tim Penhey
Let the users know if the diff has been truncated.
690
    @cachedproperty
9373.3.2 by Aaron Bentley
Prefer preview diff, fall back to review diff, cleanup.
691
    def preview_diff(self):
692
        """Return a `Diff` of the preview.
693
694
        If no preview is available, try using the review diff.
695
        """
696
        if self.context.preview_diff is not None:
697
            return self.context.preview_diff
698
        return None
699
7623.1.3 by Paul Hummer
Added review_diff method to the BMP view
700
    @property
6757.2.1 by Paul Hummer
Added support for displaying the bug and spec links in the branch merge
701
    def has_bug_or_spec(self):
702
        """Return whether or not the merge proposal has a linked bug or spec.
703
        """
704
        branch = self.context.source_branch
12505.6.1 by Ian Booth
Remove DecoratedBug and refactor mp linked_bugs to use branch.getRelatedBugTasks
705
        return self.linked_bugtasks or branch.spec_links
6757.2.1 by Paul Hummer
Added support for displaying the bug and spec links in the branch merge
706
12457.2.8 by Robert Collins
Fix branch merge proposals which use a macro now wanting bugtasks.
707
    @cachedproperty
708
    def linked_bugtasks(self):
12505.6.1 by Ian Booth
Remove DecoratedBug and refactor mp linked_bugs to use branch.getRelatedBugTasks
709
        """Return BugTasks linked to the source branch."""
12505.6.7 by Ian Booth
Test fixes
710
        return self.context.getRelatedBugTasks(self.user)
12457.2.8 by Robert Collins
Fix branch merge proposals which use a macro now wanting bugtasks.
711
9737.2.2 by Tim Penhey
Add an inline editor for commit message.
712
    @property
12268.3.8 by Tim Penhey
Refactor the TextAreaEditorWidget so it doesn't need extra HTML code in the page template.
713
    def edit_description_link_class(self):
714
        if self.context.description:
715
            return "unseen"
716
        else:
717
            return ""
718
719
    @property
7675.548.2 by Tim Penhey
Update for the description field.
720
    def description_html(self):
721
        """The description as widget HTML."""
12268.3.19 by Tim Penhey
Reorder the title parameter to match the call sites.
722
        mp = self.context
723
        description = IBranchMergeProposal['description']
724
        title = "Description of the Change"
7675.548.2 by Tim Penhey
Update for the description field.
725
        return TextAreaEditorWidget(
12268.3.19 by Tim Penhey
Reorder the title parameter to match the call sites.
726
            mp, description, title, edit_view='+edit-description')
12268.3.8 by Tim Penhey
Refactor the TextAreaEditorWidget so it doesn't need extra HTML code in the page template.
727
728
    @property
729
    def edit_commit_message_link_class(self):
730
        if self.context.commit_message:
731
            return "unseen"
732
        else:
733
            return ""
7675.548.2 by Tim Penhey
Update for the description field.
734
735
    @property
9737.2.2 by Tim Penhey
Add an inline editor for commit message.
736
    def commit_message_html(self):
737
        """The commit message as widget HTML."""
12268.3.29 by Tim Penhey
A single trailing comma can be so distructive.
738
        mp = self.context
12268.3.19 by Tim Penhey
Reorder the title parameter to match the call sites.
739
        commit_message = IBranchMergeProposal['commit_message']
740
        title = "Commit Message"
9737.2.2 by Tim Penhey
Add an inline editor for commit message.
741
        return TextAreaEditorWidget(
12268.3.19 by Tim Penhey
Reorder the title parameter to match the call sites.
742
            mp, commit_message, title, edit_view='+edit-commit-message')
9737.2.2 by Tim Penhey
Add an inline editor for commit message.
743
9848.1.5 by Paul Hummer
Got the widget working with the API.
744
    @property
745
    def status_config(self):
746
        """The config to configure the ChoiceSource JS widget."""
747
        return simplejson.dumps({
748
            'status_widget_items': vocabulary_to_choice_edit_items(
9848.1.13 by Paul Hummer
Fixed the status vocabulary to be generated based on what's enabled.
749
                self._createStatusVocabulary(),
9848.1.5 by Paul Hummer
Got the widget working with the API.
750
                css_class_prefix='mergestatus'),
751
            'status_value': self.context.queue_status.title,
7675.508.1 by Tim Penhey
Send the revision id as well.
752
            'source_revid': self.context.source_branch.last_scanned_id,
9848.1.5 by Paul Hummer
Got the widget working with the API.
753
            'user_can_edit_status': check_permission(
754
                'launchpad.Edit', self.context),
755
            })
756
5579.2.4 by Tim Penhey
UI works now.
757
6438.2.3 by Tim Penhey
Add the tables for reviews done, and requested reviews.
758
class DecoratedCodeReviewVoteReference:
759
    """Provide a code review vote that knows if it is important or not."""
760
7465.5.1 by Gary Poster
integrate lazr.config and lazr.delegates
761
    delegates(ICodeReviewVoteReference)
6438.2.3 by Tim Penhey
Add the tables for reviews done, and requested reviews.
762
7055.6.8 by Tim Penhey
Merge the two tables showing pending and completed reviews, and add nice colours everywhere.
763
    status_text_map = {
7055.6.27 by Paul Hummer
Referenced .title in status text map
764
        CodeReviewVote.DISAPPROVE: CodeReviewVote.DISAPPROVE.title,
765
        CodeReviewVote.APPROVE: CodeReviewVote.APPROVE.title,
766
        CodeReviewVote.ABSTAIN: CodeReviewVote.ABSTAIN.title,
8486.1.3 by Michael Hudson
fix
767
        CodeReviewVote.NEEDS_INFO: CodeReviewVote.NEEDS_INFO.title,
7055.6.12 by Tim Penhey
Add extra vote types, and format nicely the listing.
768
        CodeReviewVote.NEEDS_FIXING: CodeReviewVote.NEEDS_FIXING.title,
769
        CodeReviewVote.RESUBMIT: CodeReviewVote.RESUBMIT.title,
7055.6.8 by Tim Penhey
Merge the two tables showing pending and completed reviews, and add nice colours everywhere.
770
        }
771
7055.6.11 by Tim Penhey
Lots of changes to do with the request reviewer, voting and comment pages.
772
    def __init__(self, context, user, users_vote):
6438.2.3 by Tim Penhey
Add the tables for reviews done, and requested reviews.
773
        self.context = context
9461.9.1 by Aaron Bentley
Allow voting and reviewing when proposal isn't mergeable.
774
        self.can_change_review = (user == context.reviewer)
7055.6.14 by Tim Penhey
Fix the tests.
775
        if user is None:
7055.9.1 by Tim Penhey
Merge in table munging updates and resolve conflicts.
776
            self.user_can_review = False
6438.2.7 by Tim Penhey
Set the title on the star.
777
        else:
7055.6.24 by Tim Penhey
More review updates.
778
            # The user cannot review for a requested team review if the user
779
            # has already reviewed this proposal.
9461.9.1 by Aaron Bentley
Allow voting and reviewing when proposal isn't mergeable.
780
            self.user_can_review = (self.can_change_review or
781
                 (user.inTeam(context.reviewer) and (users_vote is None)))
7675.293.5 by Aaron Bentley
Restrict display of claim button.
782
        if context.reviewer == user:
783
            self.user_can_claim = False
784
        else:
785
            self.user_can_claim = self.user_can_review
7675.293.8 by Aaron Bentley
Allow reassigning code review.
786
        if user in (context.reviewer, context.registrant):
787
            self.user_can_reassign = True
788
        else:
789
            self.user_can_reassign = False
6438.2.3 by Tim Penhey
Add the tables for reviews done, and requested reviews.
790
14509.2.8 by Ian Booth
Undo move of Person.inTeam() to IPersonLimitedView and refactor view that relied on it
791
    @cachedproperty
792
    def trusted(self):
793
        """ Is the person a trusted reviewer."""
794
        proposal = self.context.branch_merge_proposal
795
        return proposal.target_branch.isPersonTrustedReviewer(
796
            self.context.reviewer)
797
6438.2.3 by Tim Penhey
Add the tables for reviews done, and requested reviews.
798
    @property
7055.8.7 by Tim Penhey
Updates following review.
799
    def show_date_requested(self):
800
        """Show the requested date if the reviewer is not the requester."""
801
        return self.context.registrant != self.context.reviewer
802
803
    @property
7055.6.8 by Tim Penhey
Merge the two tables showing pending and completed reviews, and add nice colours everywhere.
804
    def date_requested(self):
805
        """When the review was requested or None."""
7055.8.7 by Tim Penhey
Updates following review.
806
        return self.context.date_created
7055.6.8 by Tim Penhey
Merge the two tables showing pending and completed reviews, and add nice colours everywhere.
807
808
    @property
7310.1.1 by Tim Penhey
Claiming a team review without a review type didn't work.
809
    def review_type_str(self):
810
        """We want '' not 'None' if no type set."""
811
        if self.context.review_type is None:
812
            return ''
813
        return self.context.review_type
814
815
    @property
6438.2.3 by Tim Penhey
Add the tables for reviews done, and requested reviews.
816
    def date_of_comment(self):
817
        """The date of the comment, not the date_created of the vote."""
818
        return self.context.comment.message.datecreated
819
7055.6.8 by Tim Penhey
Merge the two tables showing pending and completed reviews, and add nice colours everywhere.
820
    @property
821
    def status_text(self):
822
        """The text shown in the table of the users vote."""
823
        return self.status_text_map[self.context.comment.vote]
824
7675.293.19 by Aaron Bentley
Cleanup
825
6438.2.3 by Tim Penhey
Add the tables for reviews done, and requested reviews.
826
class BranchMergeProposalVoteView(LaunchpadView):
827
    """The view used for the tables of votes and requested reviews."""
828
7055.6.8 by Tim Penhey
Merge the two tables showing pending and completed reviews, and add nice colours everywhere.
829
    @property
830
    def show_table(self):
831
        """Should the reviewer table be shown at all?
832
7055.8.7 by Tim Penhey
Updates following review.
833
        We want to show the table when there is something for the user to do
834
        with it. If the user can request a review, or is a reviewer with
835
        reviews to do, then show the table.
7055.6.8 by Tim Penhey
Merge the two tables showing pending and completed reviews, and add nice colours everywhere.
836
        """
7055.8.7 by Tim Penhey
Updates following review.
837
        # The user can request a review if the user has edit permissions, and
838
        # the branch is not in a final state.  We want to show the table as
839
        # the menu link is now shown in the table itself.
7055.6.8 by Tim Penhey
Merge the two tables showing pending and completed reviews, and add nice colours everywhere.
840
        can_request_review = (
841
            check_permission('launchpad.Edit', self.context) and
842
            self.context.isMergable())
843
7055.8.7 by Tim Penhey
Updates following review.
844
        # Show the table if there are review to show, or the user can review,
845
        # or if the user can request a review.
9461.9.2 by Aaron Bentley
Remove obsolete show_user_review_link
846
        return (len(self.reviews) > 0 or can_request_review)
7055.6.8 by Tim Penhey
Merge the two tables showing pending and completed reviews, and add nice colours everywhere.
847
6438.2.3 by Tim Penhey
Add the tables for reviews done, and requested reviews.
848
    @cachedproperty
849
    def reviews(self):
850
        """Return the decorated votes for the proposal."""
7055.6.11 by Tim Penhey
Lots of changes to do with the request reviewer, voting and comment pages.
851
        users_vote = self.context.getUsersVoteReference(self.user)
852
        return [DecoratedCodeReviewVoteReference(vote, self.user, users_vote)
14509.2.3 by Ian Booth
Allow private teams to review branches
853
                for vote in self.context.votes
854
                if check_permission('launchpad.LimitedView', vote.reviewer)]
6438.2.3 by Tim Penhey
Add the tables for reviews done, and requested reviews.
855
7055.6.8 by Tim Penhey
Merge the two tables showing pending and completed reviews, and add nice colours everywhere.
856
    @cachedproperty
6438.2.3 by Tim Penhey
Add the tables for reviews done, and requested reviews.
857
    def current_reviews(self):
858
        """The current votes ordered by vote then date."""
859
        # We want the reviews in a specific order.
860
        # Disapprovals first, then approvals, then abstentions.
7055.6.12 by Tim Penhey
Add extra vote types, and format nicely the listing.
861
        reviews = [review for review in self.reviews
862
                   if review.comment is not None]
863
        return sorted(reviews, key=operator.attrgetter('date_of_comment'),
864
                      reverse=True)
6438.2.3 by Tim Penhey
Add the tables for reviews done, and requested reviews.
865
7055.6.8 by Tim Penhey
Merge the two tables showing pending and completed reviews, and add nice colours everywhere.
866
    @cachedproperty
6438.2.3 by Tim Penhey
Add the tables for reviews done, and requested reviews.
867
    def requested_reviews(self):
868
        """Reviews requested but not yet done."""
869
        reviews = [review for review in self.reviews
870
                   if review.comment is None]
871
        # Now sort so the most recently created is first.
872
        return sorted(reviews, key=operator.attrgetter('date_created'),
873
                      reverse=True)
874
7055.6.8 by Tim Penhey
Merge the two tables showing pending and completed reviews, and add nice colours everywhere.
875
6438.3.1 by Aaron Bentley
Dirty implementation of mailing candidate reviewers
876
class IReviewRequest(Interface):
6438.3.15 by Aaron Bentley
Cleanup
877
    """Schema for requesting a review."""
6438.3.1 by Aaron Bentley
Dirty implementation of mailing candidate reviewers
878
7055.6.16 by Tim Penhey
vote tag and field name fixes.
879
    reviewer = copy_field(ICodeReviewVoteReference['reviewer'])
6438.3.1 by Aaron Bentley
Dirty implementation of mailing candidate reviewers
880
9656.1.6 by Nathan Handler
Make changes requested by Michael Hudson (mwhudson)
881
    review_type = copy_field(
11019.3.1 by Tim Penhey
Extract the DecoratedBug and DecorateBranch out of the browser/branch module and into its own. Use a DecoratedBranch for the new revisions.
882
        ICodeReviewVoteReference['review_type'],
9656.1.6 by Nathan Handler
Make changes requested by Michael Hudson (mwhudson)
883
        description=u'Lowercase keywords describing the type of review you '
884
                     'would like to be performed.')
6438.3.1 by Aaron Bentley
Dirty implementation of mailing candidate reviewers
885
886
5579.2.4 by Tim Penhey
UI works now.
887
class BranchMergeProposalRequestReviewView(LaunchpadEditFormView):
888
    """The view used to request a review of the merge proposal."""
889
6438.3.1 by Aaron Bentley
Dirty implementation of mailing candidate reviewers
890
    schema = IReviewRequest
9129.1.11 by Tim Penhey
Oops, page_title not heading.
891
    page_title = label = "Request review"
5579.2.4 by Tim Penhey
UI works now.
892
893
    @property
6438.3.1 by Aaron Bentley
Dirty implementation of mailing candidate reviewers
894
    def initial_values(self):
6438.3.15 by Aaron Bentley
Cleanup
895
        """Force the non-BMP values to None."""
7055.6.16 by Tim Penhey
vote tag and field name fixes.
896
        return {'reviewer': None, 'review_type': None}
6438.3.1 by Aaron Bentley
Dirty implementation of mailing candidate reviewers
897
898
    @property
899
    def adapters(self):
6438.3.15 by Aaron Bentley
Cleanup
900
        """Force IReviewRequest handling for BranchMergeProposal."""
6438.3.1 by Aaron Bentley
Dirty implementation of mailing candidate reviewers
901
        return {IReviewRequest: self.context}
902
903
    @property
6438.3.9 by Aaron Bentley
Support requesting additional reviewers
904
    def is_needs_review(self):
6438.3.15 by Aaron Bentley
Cleanup
905
        """Return True if queue status is NEEDS_REVIEW."""
6438.3.9 by Aaron Bentley
Support requesting additional reviewers
906
        return (self.context.queue_status ==
907
            BranchMergeProposalStatus.NEEDS_REVIEW)
908
909
    @property
5579.2.4 by Tim Penhey
UI works now.
910
    def next_url(self):
911
        return canonical_url(self.context)
912
6618.2.1 by Paul Hummer
Adds cancel_url, fixes tests
913
    cancel_url = next_url
6438.3.24 by Aaron Bentley
Updates for LP2.0
914
6677.1.3 by Jonathan Lange
Only access IEmailAddress.email once.
915
    def requestReview(self, candidate, review_type):
6677.1.4 by Jonathan Lange
Respond to review comments.
916
        """Request a `review_type` review from `candidate` and email them."""
11019.3.1 by Tim Penhey
Extract the DecoratedBug and DecorateBranch out of the browser/branch module and into its own. Use a DecoratedBranch for the new revisions.
917
        self.context.nominateReviewer(candidate, self.user, review_type)
6677.1.3 by Jonathan Lange
Only access IEmailAddress.email once.
918
7055.6.16 by Tim Penhey
vote tag and field name fixes.
919
    @action('Request Review', name='review')
6438.3.1 by Aaron Bentley
Dirty implementation of mailing candidate reviewers
920
    @notify
5579.2.4 by Tim Penhey
UI works now.
921
    def review_action(self, action, data):
6438.3.15 by Aaron Bentley
Cleanup
922
        """Set 'Needs review' status, nominate reviewers, send emails."""
5579.2.4 by Tim Penhey
UI works now.
923
        self.context.requestReview()
7055.6.16 by Tim Penhey
vote tag and field name fixes.
924
        candidate = data.pop('reviewer', None)
6438.3.1 by Aaron Bentley
Dirty implementation of mailing candidate reviewers
925
        review_type = data.pop('review_type', None)
926
        if candidate is not None:
6677.1.3 by Jonathan Lange
Only access IEmailAddress.email once.
927
            self.requestReview(candidate, review_type)
5579.2.4 by Tim Penhey
UI works now.
928
929
    def validate(self, data):
930
        """Ensure that the proposal is in an appropriate state."""
7055.6.8 by Tim Penhey
Merge the two tables showing pending and completed reviews, and add nice colours everywhere.
931
        if not self.context.isMergable():
5579.2.4 by Tim Penhey
UI works now.
932
            self.addError("The merge proposal is not an a valid state to "
933
                          "mark as 'Needs review'.")
934
935
936
class ReviewForm(Interface):
937
    """A simple interface to define the revision number field."""
938
939
    revision_number = Int(
940
        title=_("Reviewed Revision"), required=True,
941
        description=_("The revision number on the source branch which "
942
                      "has been reviewed."))
943
944
    whiteboard = Whiteboard(
945
        title=_('Whiteboard'), required=False,
946
        description=_('Notes about the merge.'))
947
948
949
class MergeProposalEditView(LaunchpadEditFormView,
950
                            BranchMergeProposalRevisionIdMixin):
951
    """A base class for merge proposal edit views."""
5280.4.3 by Tim Penhey
Workflow works nicely, resisting hard gold plating.
952
6618.2.1 by Paul Hummer
Adds cancel_url, fixes tests
953
    def initialize(self):
6618.2.4 by Paul Hummer
Adds merge conditional coding style change
954
        # Record next_url and cancel url now
6618.2.1 by Paul Hummer
Adds cancel_url, fixes tests
955
        self.next_url = canonical_url(self.context)
956
        self.cancel_url = self.next_url
957
        super(MergeProposalEditView, self).initialize()
958
5763.1.6 by Tim Penhey
Some view refactoring.
959
    def _getRevisionId(self, data):
960
        """Translate the revision number that was entered into a revision id.
961
962
        If the branch is REMOTE we won't have any scanned revisions to compare
963
        against, so store the raw integer revision number as the revision id.
5280.4.3 by Tim Penhey
Workflow works nicely, resisting hard gold plating.
964
        """
965
        source_branch = self.context.source_branch
5763.1.6 by Tim Penhey
Some view refactoring.
966
        # Get the revision number out of the data.
5280.4.3 by Tim Penhey
Workflow works nicely, resisting hard gold plating.
967
        if source_branch.branch_type == BranchType.REMOTE:
6623.1.1 by Tim Penhey
Stringify the revision number for remote branches.
968
            return str(data.pop('revision_number'))
5280.4.3 by Tim Penhey
Workflow works nicely, resisting hard gold plating.
969
        else:
5763.1.6 by Tim Penhey
Some view refactoring.
970
            branch_revision = source_branch.getBranchRevision(
6736.3.5 by Tim Penhey
Clean up the Branch.getBranchRevision method.
971
                sequence=data.pop('revision_number'))
5763.1.6 by Tim Penhey
Some view refactoring.
972
            return branch_revision.revision.revision_id
973
5579.2.1 by Tim Penhey
Some queue management
974
    def _validateRevisionNumber(self, data, revision_name):
975
        """Check to make sure that the revision number entered is valid."""
5763.1.6 by Tim Penhey
Some view refactoring.
976
        rev_no = data.get('revision_number')
977
        if rev_no is not None:
978
            try:
979
                rev_no = int(rev_no)
980
            except ValueError:
981
                self.setFieldError(
982
                    'revision_number',
5579.2.1 by Tim Penhey
Some queue management
983
                    'The %s revision must be a positive number.'
984
                    % revision_name)
5280.4.3 by Tim Penhey
Workflow works nicely, resisting hard gold plating.
985
            else:
5763.1.6 by Tim Penhey
Some view refactoring.
986
                if rev_no < 1:
987
                    self.setFieldError(
988
                        'revision_number',
5579.2.1 by Tim Penhey
Some queue management
989
                        'The %s revision must be a positive number.'
990
                        % revision_name)
5763.1.6 by Tim Penhey
Some view refactoring.
991
                # Accept any positive integer for a REMOTE branch.
992
                source_branch = self.context.source_branch
993
                if (source_branch.branch_type != BranchType.REMOTE and
994
                    rev_no > source_branch.revision_count):
995
                    self.setFieldError(
996
                        'revision_number',
5579.2.1 by Tim Penhey
Some queue management
997
                        'The %s revision cannot be larger than the '
998
                        'tip revision of the source branch.'
999
                        % revision_name)
1000
1001
11812.2.3 by Aaron Bentley
Add break link option to resubmit.
1002
class ResubmitSchema(IBranchMergeProposal):
1003
1004
    break_link = Bool(
11812.2.11 by Aaron Bentley
Change text for breaking link.
1005
        title=u'Start afresh',
1006
        description=(
1007
            u'Do not show old conversation and do not link to superseded'
1008
            ' proposal.'),
11812.2.3 by Aaron Bentley
Add break link option to resubmit.
1009
        default=False,)
1010
1011
11812.2.4 by Aaron Bentley
Switch to LaunchpadFormView to avoid overriding render_context.
1012
class BranchMergeProposalResubmitView(LaunchpadFormView,
5579.2.4 by Tim Penhey
UI works now.
1013
                                      UnmergedRevisionsMixin):
1014
    """The view to resubmit a proposal to merge."""
1015
11812.2.3 by Aaron Bentley
Add break link option to resubmit.
1016
    schema = ResubmitSchema
11812.2.2 by Aaron Bentley
Provide new resubmit UI.
1017
    for_input = True
9129.1.11 by Tim Penhey
Oops, page_title not heading.
1018
    page_title = label = "Resubmit proposal to merge"
11812.2.2 by Aaron Bentley
Provide new resubmit UI.
1019
    field_names = [
11812.2.10 by Aaron Bentley
Updates from review.
1020
        'source_branch',
1021
        'target_branch',
1022
        'prerequisite_branch',
1023
        'description',
1024
        'break_link',
11812.2.2 by Aaron Bentley
Provide new resubmit UI.
1025
        ]
11812.2.10 by Aaron Bentley
Updates from review.
1026
1027
    def initialize(self):
1028
        self.cancel_url = canonical_url(self.context)
1029
        super(BranchMergeProposalResubmitView, self).initialize()
1030
11812.2.3 by Aaron Bentley
Add break link option to resubmit.
1031
    @property
1032
    def initial_values(self):
1033
        UNSET = object()
11812.2.10 by Aaron Bentley
Updates from review.
1034
        items = ((key, getattr(self.context, key, UNSET)) for key in
11812.2.3 by Aaron Bentley
Add break link option to resubmit.
1035
                  self.field_names if key != 'break_link')
11812.2.10 by Aaron Bentley
Updates from review.
1036
        return dict(item for item in items if item[1] is not UNSET)
5579.2.4 by Tim Penhey
UI works now.
1037
1038
    @action('Resubmit', name='resubmit')
1039
    def resubmit_action(self, action, data):
1040
        """Resubmit this proposal."""
13278.2.4 by Aaron Bentley
Handle BranchMergeProposalExists as a user error.
1041
        try:
1042
            proposal = self.context.resubmit(
1043
                self.user, data['source_branch'], data['target_branch'],
1044
                data['prerequisite_branch'], data['description'],
1045
                data['break_link'])
1046
        except BranchMergeProposalExists as e:
1047
            message = structured(
13278.2.7 by Aaron Bentley
Tweak error and test.
1048
                'Cannot resubmit because <a href="%(url)s">a similar merge'
13278.2.4 by Aaron Bentley
Handle BranchMergeProposalExists as a user error.
1049
                ' proposal</a> is already active.',
1050
                url=canonical_url(e.existing_proposal))
1051
            self.request.response.addErrorNotification(message)
1052
            self.next_url = canonical_url(self.context)
1053
            return None
13618.3.1 by Aaron Bentley
InvalidMergeProposal is handled as a user error.
1054
        except InvalidBranchMergeProposal as e:
1055
            self.addError(str(e))
1056
            return None
7055.6.17 by Tim Penhey
Fixed resubmit and link visibility.
1057
        self.next_url = canonical_url(proposal)
11812.2.2 by Aaron Bentley
Provide new resubmit UI.
1058
        return proposal
5579.2.4 by Tim Penhey
UI works now.
1059
1060
5280.4.3 by Tim Penhey
Workflow works nicely, resisting hard gold plating.
1061
class BranchMergeProposalEditView(MergeProposalEditView):
5280.4.24 by Tim Penhey
First cut of changes following review.
1062
    """The view to control the editing of merge proposals."""
4414.5.13 by Tim Penhey
Still work in progress
1063
    schema = IBranchMergeProposal
9129.1.11 by Tim Penhey
Oops, page_title not heading.
1064
    page_title = label = "Edit branch merge proposal"
6699.2.2 by Paul Hummer
Adds commit_message field
1065
    field_names = ["commit_message", "whiteboard"]
4414.5.13 by Tim Penhey
Still work in progress
1066
5280.4.24 by Tim Penhey
First cut of changes following review.
1067
    @action('Update', name='update')
1068
    def update_action(self, action, data):
1069
        """Update the whiteboard and go back to the source branch."""
1070
        self.updateContextFromData(data)
1071
1072
7954.3.1 by Paul Hummer
Added BranchMergeProposalCommitMessageEditView
1073
class BranchMergeProposalCommitMessageEditView(MergeProposalEditView):
1074
    """The view to edit the commit message of merge proposals."""
1075
7954.3.2 by Paul Hummer
Added commit message editing view stubs
1076
    schema = IBranchMergeProposal
1077
    label = "Edit merge proposal commit message"
9084.3.6 by Tim Penhey
Remove the commit message template in favour of generic-edit.
1078
    page_title = label
7954.3.2 by Paul Hummer
Added commit message editing view stubs
1079
    field_names = ['commit_message']
1080
1081
    @action('Update', name='update')
1082
    def update_action(self, action, data):
1083
        """Update the commit message."""
7954.3.4 by Paul Hummer
Got an end-to-end working change for commit message
1084
        self.updateContextFromData(data)
7954.3.2 by Paul Hummer
Added commit message editing view stubs
1085
7954.3.1 by Paul Hummer
Added BranchMergeProposalCommitMessageEditView
1086
7675.548.2 by Tim Penhey
Update for the description field.
1087
class BranchMergeProposalDescriptionEditView(MergeProposalEditView):
1088
    """The view to edit the description of merge proposals."""
1089
1090
    schema = IBranchMergeProposal
1091
    label = "Edit merge proposal description"
1092
    page_title = label
1093
    field_names = ['description']
1094
1095
    @action('Update', name='update')
1096
    def update_action(self, action, data):
1097
        """Update the commit message."""
1098
        self.updateContextFromData(data)
1099
1100
5280.4.24 by Tim Penhey
First cut of changes following review.
1101
class BranchMergeProposalDeleteView(MergeProposalEditView):
1102
    """The view to control the deletion of merge proposals."""
1103
    schema = IBranchMergeProposal
1104
    field_names = []
9129.1.11 by Tim Penhey
Oops, page_title not heading.
1105
    page_title = label = 'Delete proposal to merge branch'
5280.4.24 by Tim Penhey
First cut of changes following review.
1106
4414.5.13 by Tim Penhey
Still work in progress
1107
    def initialize(self):
4414.5.14 by Tim Penhey
SOP and view work
1108
        # Store the source branch for `next_url` to make sure that
1109
        # it is available in the situation where the merge proposal
1110
        # is deleted.
4414.5.13 by Tim Penhey
Still work in progress
1111
        self.source_branch = self.context.source_branch
5280.4.24 by Tim Penhey
First cut of changes following review.
1112
        super(BranchMergeProposalDeleteView, self).initialize()
4414.5.13 by Tim Penhey
Still work in progress
1113
5280.4.14 by Tim Penhey
More ui tweaks.
1114
    @action('Delete proposal', name='delete')
4414.5.13 by Tim Penhey
Still work in progress
1115
    def delete_action(self, action, data):
1116
        """Delete the merge proposal and go back to the source branch."""
5600.2.7 by Tim Penhey
Updated pagetest for resubmissions.
1117
        self.context.deleteProposal()
5280.4.11 by Tim Penhey
Read only page is now the default view, edit pages hang off that.
1118
        # Override the next url to be the source branch.
6618.2.5 by Paul Hummer
Fixes tests
1119
        self.next_url = canonical_url(self.source_branch)
4414.5.13 by Tim Penhey
Still work in progress
1120
1121
1122
class BranchMergeProposalMergedView(LaunchpadEditFormView):
4414.5.14 by Tim Penhey
SOP and view work
1123
    """The view to mark a merge proposal as merged."""
4414.5.13 by Tim Penhey
Still work in progress
1124
    schema = IBranchMergeProposal
9129.1.11 by Tim Penhey
Oops, page_title not heading.
1125
    page_title = label = "Edit branch merge proposal"
4414.5.13 by Tim Penhey
Still work in progress
1126
    field_names = ["merged_revno"]
7325.8.28 by Paul Hummer
Fixed webapp setting of merged_revno to use markAsMerged
1127
    for_input = True
4414.5.13 by Tim Penhey
Still work in progress
1128
5280.4.11 by Tim Penhey
Read only page is now the default view, edit pages hang off that.
1129
    @property
5280.4.24 by Tim Penhey
First cut of changes following review.
1130
    def initial_values(self):
6669.1.2 by Jonathan Lange
Fix up the comment.
1131
        # Default to the tip of the target branch, on the assumption that the
1132
        # source branch has just been merged into it.
7055.8.7 by Tim Penhey
Updates following review.
1133
        if self.context.merged_revno is not None:
1134
            revno = self.context.merged_revno
1135
        else:
1136
            revno = self.context.target_branch.revision_count
1137
        return {'merged_revno': revno}
5280.4.24 by Tim Penhey
First cut of changes following review.
1138
1139
    @property
5280.4.11 by Tim Penhey
Read only page is now the default view, edit pages hang off that.
1140
    def next_url(self):
1141
        return canonical_url(self.context)
1142
6618.2.1 by Paul Hummer
Adds cancel_url, fixes tests
1143
    cancel_url = next_url
1144
4414.5.13 by Tim Penhey
Still work in progress
1145
    @action('Mark as Merged', name='mark_merged')
6080.6.1 by Aaron Bentley
Refactor notify decorator out of update_and_notify
1146
    @notify
4414.5.13 by Tim Penhey
Still work in progress
1147
    def mark_merged_action(self, action, data):
1148
        """Update the whiteboard and go back to the source branch."""
6853.1.4 by Paul Hummer
Added a way to edit the branch merge proposal's merged revno
1149
        revno = data['merged_revno']
6042.2.2 by Tim Penhey
Updates following review.
1150
        if self.context.queue_status == BranchMergeProposalStatus.MERGED:
7325.8.28 by Paul Hummer
Fixed webapp setting of merged_revno to use markAsMerged
1151
            self.context.markAsMerged(merged_revno=revno)
6853.1.4 by Paul Hummer
Added a way to edit the branch merge proposal's merged revno
1152
            self.request.response.addNotification(
1153
                'The proposal\'s merged revision has been updated.')
6042.2.2 by Tim Penhey
Updates following review.
1154
        else:
6080.6.1 by Aaron Bentley
Refactor notify decorator out of update_and_notify
1155
            self.context.markAsMerged(revno, merge_reporter=self.user)
1156
            self.request.response.addNotification(
1157
                'The proposal has now been marked as merged.')
4414.5.13 by Tim Penhey
Still work in progress
1158
1159
    def validate(self, data):
1160
        # Ensure a positive integer value.
1161
        revno = data.get('merged_revno')
1162
        if revno is not None:
1163
            if revno <= 0:
1164
                self.setFieldError(
1165
                    'merged_revno',
1166
                    'Revision numbers must be positive integers.')
5579.2.1 by Tim Penhey
Some queue management
1167
1168
1169
class EnqueueForm(Interface):
1170
    """A simple interface to populate the form to enqueue a proposal."""
1171
1172
    revision_number = Int(
1173
        title=_("Queue Revision"), required=True,
5579.2.11 by Tim Penhey
Updates following review.
1174
        description=_("The revision number of the source branch "
5579.2.1 by Tim Penhey
Some queue management
1175
                      "which is to be merged into the target branch."))
1176
1177
    commit_message = Summary(
1178
        title=_("Commit Message"), required=True,
1179
        description=_("The commit message to be used when merging "
1180
                      "the source branch."))
1181
1182
    whiteboard = Whiteboard(
1183
        title=_('Whiteboard'), required=False,
1184
        description=_('Notes about the merge.'))
1185
1186
1187
class BranchMergeProposalEnqueueView(MergeProposalEditView,
1188
                                     UnmergedRevisionsMixin):
5579.2.11 by Tim Penhey
Updates following review.
1189
    """The view to submit a merge proposal for merging."""
1190
5579.2.1 by Tim Penhey
Some queue management
1191
    schema = EnqueueForm
9129.1.11 by Tim Penhey
Oops, page_title not heading.
1192
    page_title = label = "Queue branch for merging"
5579.2.1 by Tim Penhey
Some queue management
1193
1194
    @property
5579.2.7 by Tim Penhey
Update the existing pagetest for new actions.
1195
    def initial_values(self):
1196
        # If the user is a valid reviewer, then default the revision
1197
        # number to be the tip.
9041.4.1 by Tim Penhey
Move isPersonTrustedReviewer to the IBranch interface.
1198
        if self.context.target_branch.isPersonTrustedReviewer(self.user):
5579.2.7 by Tim Penhey
Update the existing pagetest for new actions.
1199
            revision_number = self.context.source_branch.revision_count
1200
        else:
1201
            revision_number = self._getRevisionNumberForRevisionId(
1202
                self.context.reviewed_revision_id)
1203
1204
        return {'revision_number': revision_number}
1205
1206
    @property
5579.2.1 by Tim Penhey
Some queue management
1207
    def adapters(self):
5579.2.11 by Tim Penhey
Updates following review.
1208
        """See `LaunchpadFormView`"""
5579.2.1 by Tim Penhey
Some queue management
1209
        return {EnqueueForm: self.context}
1210
5579.2.7 by Tim Penhey
Update the existing pagetest for new actions.
1211
    def setUpFields(self):
1212
        super(BranchMergeProposalEnqueueView, self).setUpFields()
1213
        # If the user is not a valid reviewer for the target branch,
5579.2.11 by Tim Penhey
Updates following review.
1214
        # then the revision number should be read only, so an
1215
        # untrusted user cannot land changes that have not bee reviewed.
9041.4.1 by Tim Penhey
Move isPersonTrustedReviewer to the IBranch interface.
1216
        if not self.context.target_branch.isPersonTrustedReviewer(self.user):
5579.2.7 by Tim Penhey
Update the existing pagetest for new actions.
1217
            self.form_fields['revision_number'].for_display = True
1218
5579.2.1 by Tim Penhey
Some queue management
1219
    @action('Enqueue', name='enqueue')
6080.5.9 by Aaron Bentley
Ensure notifications show enqueing
1220
    @update_and_notify
5579.2.1 by Tim Penhey
Some queue management
1221
    def enqueue_action(self, action, data):
5579.2.11 by Tim Penhey
Updates following review.
1222
        """Update the whiteboard and enqueue the merge proposal."""
9041.4.1 by Tim Penhey
Move isPersonTrustedReviewer to the IBranch interface.
1223
        if self.context.target_branch.isPersonTrustedReviewer(self.user):
5579.2.7 by Tim Penhey
Update the existing pagetest for new actions.
1224
            revision_id = self._getRevisionId(data)
1225
        else:
1226
            revision_id = self.context.reviewed_revision_id
1227
        self.context.enqueue(self.user, revision_id)
5579.2.1 by Tim Penhey
Some queue management
1228
1229
    def validate(self, data):
5579.2.11 by Tim Penhey
Updates following review.
1230
        """Make sure that the proposal has been reviewed.
1231
1232
        Or that the logged in user is able to review the branch as well.
1233
        """
5579.2.7 by Tim Penhey
Update the existing pagetest for new actions.
1234
        if not self.context.isValidTransition(
1235
            BranchMergeProposalStatus.QUEUED, self.user):
1236
            self.addError(
5579.2.11 by Tim Penhey
Updates following review.
1237
                "The merge proposal is cannot be queued as it has not "
1238
                "been reviewed.")
5579.2.1 by Tim Penhey
Some queue management
1239
1240
        self._validateRevisionNumber(data, 'enqueued')
1241
1242
1243
class BranchMergeProposalDequeueView(LaunchpadEditFormView):
1244
    """The view to remove a merge proposal from the merge queue."""
1245
1246
    schema = IBranchMergeProposal
5579.2.4 by Tim Penhey
UI works now.
1247
    field_names = ["whiteboard"]
9129.1.11 by Tim Penhey
Oops, page_title not heading.
1248
    page_title = label = "Dequeue branch"
5579.2.1 by Tim Penhey
Some queue management
1249
1250
    @property
1251
    def next_url(self):
1252
        return canonical_url(self.context)
1253
6618.2.1 by Paul Hummer
Adds cancel_url, fixes tests
1254
    cancel_url = next_url
1255
5579.2.4 by Tim Penhey
UI works now.
1256
    @action('Dequeue', name='dequeue')
6080.5.17 by Aaron Bentley
Implement notification for the other dequeue
1257
    @update_and_notify
5579.2.4 by Tim Penhey
UI works now.
1258
    def dequeue_action(self, action, data):
5579.2.11 by Tim Penhey
Updates following review.
1259
        """Update the whiteboard and remove the proposal from the queue."""
5579.2.4 by Tim Penhey
UI works now.
1260
        self.context.dequeue()
5579.2.1 by Tim Penhey
Some queue management
1261
5579.2.4 by Tim Penhey
UI works now.
1262
    def validate(self, data):
5579.2.11 by Tim Penhey
Updates following review.
1263
        """Make sure the proposal is queued before removing."""
5579.2.4 by Tim Penhey
UI works now.
1264
        if self.context.queue_status != BranchMergeProposalStatus.QUEUED:
1265
            self.addError("The merge proposal is not queued.")
1266
1267
1268
class BranchMergeProposalInlineDequeueView(LaunchpadEditFormView):
1269
    """The view to provide a 'dequeue' button to the queue view."""
1270
1271
    schema = IBranchMergeProposal
1272
    field_names = []
1273
1274
    @property
1275
    def next_url(self):
1276
        return canonical_url(self.context.target_branch) + '/+merge-queue'
1277
6618.2.1 by Paul Hummer
Adds cancel_url, fixes tests
1278
    cancel_url = next_url
1279
5579.2.4 by Tim Penhey
UI works now.
1280
    @action('Dequeue', name='dequeue')
6080.5.16 by Aaron Bentley
Notification on dequeue
1281
    @notify
5579.2.4 by Tim Penhey
UI works now.
1282
    def dequeue_action(self, action, data):
5579.2.11 by Tim Penhey
Updates following review.
1283
        """Remove the proposal from the queue if queued."""
5579.2.4 by Tim Penhey
UI works now.
1284
        if self.context.queue_status == BranchMergeProposalStatus.QUEUED:
1285
            self.context.dequeue()
1286
1287
    @property
1288
    def prefix(self):
1289
        return "field%s" % self.context.id
1290
1291
    @property
1292
    def action_url(self):
1293
        return "%s/+dequeue-inline" % canonical_url(self.context)
1294
1295
1296
class BranchMergeProposalJumpQueueView(LaunchpadEditFormView):
1297
    """The view to provide a move the proposal to the front of the queue."""
1298
1299
    schema = IBranchMergeProposal
1300
    field_names = []
1301
1302
    @property
1303
    def next_url(self):
1304
        return canonical_url(self.context.target_branch) + '/+merge-queue'
1305
1306
    @action('Move to front', name='move')
6080.5.15 by Aaron Bentley
Notify on queue position changes
1307
    @notify
5579.2.4 by Tim Penhey
UI works now.
1308
    def move_action(self, action, data):
5579.2.11 by Tim Penhey
Updates following review.
1309
        """Move the proposal to the front of the queue (if queued)."""
5579.2.4 by Tim Penhey
UI works now.
1310
        if (self.context.queue_status == BranchMergeProposalStatus.QUEUED and
1311
            check_permission('launchpad.Edit', self.context.target_branch)):
1312
            self.context.moveToFrontOfQueue()
1313
1314
    @property
1315
    def prefix(self):
1316
        return "field%s" % self.context.id
1317
1318
    @property
1319
    def action_url(self):
1320
        return "%s/+jump-queue" % canonical_url(self.context)
6699.2.1 by Paul Hummer
Modifies views for some of the subscription stuff
1321
1322
1323
class BranchMergeProposalSubscribersView(LaunchpadView):
1324
    """Used to show the pagelet subscribers on the main proposal page."""
1325
1326
    def initialize(self):
1327
        """See `LaunchpadView`."""
1328
        # Get the subscribers and dump them into two sets.
1329
        self._full_subscribers = set()
1330
        self._status_subscribers = set()
1331
        # Add subscribers from the source and target branches.
1332
        self._add_subscribers_for_branch(self.context.source_branch)
1333
        self._add_subscribers_for_branch(self.context.target_branch)
1334
        # Remove all the people from the comment_subscribers from the
1335
        # status_and_vote_subscribers as they recipients will get the email
1336
        # only once, and for the most detailed subscription from the source
1337
        # and target branches.
1338
        self._status_subscribers = (
1339
            self._status_subscribers - self._full_subscribers)
1340
1341
    def _add_subscribers_for_branch(self, branch):
1342
        """Add the subscribers to the subscription sets for the branch."""
1343
        for subscription in branch.subscriptions:
1344
            level = subscription.review_level
1345
            if level == CodeReviewNotificationLevel.FULL:
1346
                self._full_subscribers.add(subscription.person)
1347
            elif level == CodeReviewNotificationLevel.STATUS:
1348
                self._status_subscribers.add(subscription.person)
1349
            else:
1350
                # We don't do anything right now with people who say they
1351
                # don't want to see anything.
1352
                pass
1353
1354
    @cachedproperty
1355
    def full_subscribers(self):
1356
        """A list of full subscribers ordered by displayname."""
1357
        return sorted(
1358
            self._full_subscribers, key=operator.attrgetter('displayname'))
1359
1360
    @cachedproperty
1361
    def status_subscribers(self):
1362
        """A list of full subscribers ordered by displayname."""
1363
        return sorted(
1364
            self._status_subscribers, key=operator.attrgetter('displayname'))
1365
1366
    @property
1367
    def has_subscribers(self):
1368
        """True if there are subscribers to the branch."""
1369
        return len(self.full_subscribers) + len(self.status_subscribers)
1370
7055.6.6 by Tim Penhey
Add an edit status view for merge proposals and remove the menu links.
1371
9848.1.13 by Paul Hummer
Fixed the status vocabulary to be generated based on what's enabled.
1372
class BranchMergeProposalChangeStatusView(MergeProposalEditView,
1373
                                          BranchMergeProposalStatusMixin):
7055.6.6 by Tim Penhey
Add an edit status view for merge proposals and remove the menu links.
1374
9129.1.11 by Tim Penhey
Oops, page_title not heading.
1375
    page_title = label = "Change merge proposal status"
7055.6.6 by Tim Penhey
Add an edit status view for merge proposals and remove the menu links.
1376
    schema = IBranchMergeProposal
1377
    field_names = []
1378
7055.7.9 by Tim Penhey
Add test for +edit-status vocabulary options.
1379
    def setUpFields(self):
1380
        MergeProposalEditView.setUpFields(self)
1381
        # Add the custom restricted queue status widget.
1382
        status_field = self.schema['queue_status']
7055.6.6 by Tim Penhey
Add an edit status view for merge proposals and remove the menu links.
1383
1384
        status_choice = Choice(
1385
                __name__='queue_status', title=status_field.title,
7055.7.9 by Tim Penhey
Add test for +edit-status vocabulary options.
1386
                required=True, vocabulary=self._createStatusVocabulary())
7055.6.6 by Tim Penhey
Add an edit status view for merge proposals and remove the menu links.
1387
        status_field = form.Fields(
1388
            status_choice, render_context=self.render_context)
1389
        self.form_fields = status_field + self.form_fields
1390
1391
    @action('Change Status', name='update')
7055.7.5 by Tim Penhey
Fire email notifications.
1392
    @notify
7055.6.6 by Tim Penhey
Add an edit status view for merge proposals and remove the menu links.
1393
    def update_action(self, action, data):
1394
        """Update the status."""
1395
1396
        curr_status = self.context.queue_status
1397
        # Assume for now that the queue_status in the data is a valid
1398
        # transition from where we are.
1399
        rev_id = self.request.form['revno']
1400
        new_status = data['queue_status']
1401
        # Don't do anything if the user hasn't changed the status.
1402
        if new_status == curr_status:
1403
            return
1404
9327.2.2 by Aaron Bentley
Remove 'resubmit'/ superseded status setting.
1405
        assert new_status != BranchMergeProposalStatus.SUPERSEDED, (
1406
            'Superseded is done via an action, not by setting status.')
1407
        self.context.setStatus(new_status, self.user, rev_id)
7055.6.6 by Tim Penhey
Add an edit status view for merge proposals and remove the menu links.
1408
7055.6.11 by Tim Penhey
Lots of changes to do with the request reviewer, voting and comment pages.
1409
1410
class IAddVote(Interface):
1411
    """Interface for use as a schema for CodeReviewComment forms."""
1412
7055.6.16 by Tim Penhey
vote tag and field name fixes.
1413
    vote = copy_field(ICodeReviewComment['vote'], required=True)
7055.6.11 by Tim Penhey
Lots of changes to do with the request reviewer, voting and comment pages.
1414
9656.1.6 by Nathan Handler
Make changes requested by Michael Hudson (mwhudson)
1415
    review_type = copy_field(
11019.3.1 by Tim Penhey
Extract the DecoratedBug and DecorateBranch out of the browser/branch module and into its own. Use a DecoratedBranch for the new revisions.
1416
        ICodeReviewVoteReference['review_type'],
9656.1.6 by Nathan Handler
Make changes requested by Michael Hudson (mwhudson)
1417
        description=u'Lowercase keywords describing the type of review you '
1418
                     'are performing.')
7055.6.11 by Tim Penhey
Lots of changes to do with the request reviewer, voting and comment pages.
1419
1420
    comment = Text(title=_('Comment'), required=False)
1421
1422
1423
class BranchMergeProposalAddVoteView(LaunchpadFormView):
1424
    """View for adding a CodeReviewComment."""
1425
1426
    schema = IAddVote
1427
    field_names = ['vote', 'review_type', 'comment']
1428
7285.2.1 by Paul Hummer
Added class to the comment form in a reply
1429
    custom_widget('comment', TextAreaWidget, cssClass='codereviewcomment')
1430
7055.6.11 by Tim Penhey
Lots of changes to do with the request reviewer, voting and comment pages.
1431
    @cachedproperty
1432
    def initial_values(self):
7055.6.24 by Tim Penhey
More review updates.
1433
        """The initial values are used to populate the form fields."""
7055.6.11 by Tim Penhey
Lots of changes to do with the request reviewer, voting and comment pages.
1434
        # Look to see if there is a vote reference already for the user.
1435
        if self.users_vote_ref is None:
1436
            # Look at the request to see if there is something there.
1437
            review_type = self.request.form.get('review_type', '')
1438
        else:
1439
            review_type = self.users_vote_ref.review_type
7055.6.24 by Tim Penhey
More review updates.
1440
        # We'll be positive here and default the vote to approve.
7055.6.11 by Tim Penhey
Lots of changes to do with the request reviewer, voting and comment pages.
1441
        return {'vote': CodeReviewVote.APPROVE,
1442
                'review_type': review_type}
1443
1444
    def initialize(self):
1445
        """Get the users existing vote reference."""
1446
        self.users_vote_ref = self.context.getUsersVoteReference(self.user)
1447
        # If the user is not in the review team, nor in any team that has been
7055.6.24 by Tim Penhey
More review updates.
1448
        # requested to review and doesn't already have a vote reference, then
7055.6.11 by Tim Penhey
Lots of changes to do with the request reviewer, voting and comment pages.
1449
        # error out as the user must have URL hacked to get here.
7055.6.12 by Tim Penhey
Add extra vote types, and format nicely the listing.
1450
7055.9.2 by Tim Penhey
Review updates.
1451
        # XXX: Tim Penhey, 2008-10-02, bug=277000
1452
        # Move valid_voter db class to expose for API.
7055.6.24 by Tim Penhey
More review updates.
1453
7055.6.11 by Tim Penhey
Lots of changes to do with the request reviewer, voting and comment pages.
1454
        if self.user is None:
7055.6.24 by Tim Penhey
More review updates.
1455
            # Anonymous users are not valid voters.
7055.6.11 by Tim Penhey
Lots of changes to do with the request reviewer, voting and comment pages.
1456
            raise AssertionError('Invalid voter')
1457
        LaunchpadFormView.initialize(self)
1458
1459
    def setUpFields(self):
1460
        LaunchpadFormView.setUpFields(self)
1461
        self.reviewer = self.user.name
7055.6.30 by Paul Hummer
Added technical debt *sigh*
1462
        # claim_review is set in situations where a user is reviewing on
1463
        # behalf of a team.
7055.6.11 by Tim Penhey
Lots of changes to do with the request reviewer, voting and comment pages.
1464
        claim_review = self.request.form.get('claim')
1465
        if claim_review and self.users_vote_ref is None:
1466
            team = getUtility(IPersonSet).getByName(claim_review)
1467
            if team is not None and self.user.inTeam(team):
7310.1.1 by Tim Penhey
Claiming a team review without a review type didn't work.
1468
                # If the review type is None, then don't show the field.
10147.3.1 by Tim Penhey
make the review claiming work for empty review types too
1469
                self.reviewer = team.name
7310.1.1 by Tim Penhey
Claiming a team review without a review type didn't work.
1470
                if self.initial_values['review_type'] == '':
1471
                    self.form_fields = self.form_fields.omit('review_type')
1472
                else:
1473
                    # Disable the review_type field
1474
                    self.form_fields['review_type'].for_display = True
7055.6.11 by Tim Penhey
Lots of changes to do with the request reviewer, voting and comment pages.
1475
1476
    @property
1477
    def label(self):
1478
        """The pagetitle and heading."""
7055.6.16 by Tim Penhey
vote tag and field name fixes.
1479
        return "Review merge proposal for %s" % (
7055.6.11 by Tim Penhey
Lots of changes to do with the request reviewer, voting and comment pages.
1480
            self.context.source_branch.bzr_identity)
9129.1.11 by Tim Penhey
Oops, page_title not heading.
1481
    page_title = label
7055.6.11 by Tim Penhey
Lots of changes to do with the request reviewer, voting and comment pages.
1482
7055.6.16 by Tim Penhey
vote tag and field name fixes.
1483
    @action('Save Review', name='vote')
7055.6.11 by Tim Penhey
Lots of changes to do with the request reviewer, voting and comment pages.
1484
    def vote_action(self, action, data):
7055.6.23 by Tim Penhey
Updates following jml's review.
1485
        """Create the comment."""
7055.6.24 by Tim Penhey
More review updates.
1486
        # Get the review type from the data dict.  If the setUpFields set the
1487
        # review_type field as for_display then 'review_type' will not be in
1488
        # the data dict.  If this is the case, get the review_type from the
1489
        # hidden field that we so cunningly added to the form.
7055.6.11 by Tim Penhey
Lots of changes to do with the request reviewer, voting and comment pages.
1490
        review_type = data.get(
1491
            'review_type',
1492
            self.request.form.get('review_type'))
7310.1.1 by Tim Penhey
Claiming a team review without a review type didn't work.
1493
        # Translate the request parameter back into what our object model
1494
        # needs.
1495
        if review_type == '':
1496
            review_type = None
7055.6.11 by Tim Penhey
Lots of changes to do with the request reviewer, voting and comment pages.
1497
        # Get the reviewer from the hidden input.
1498
        reviewer_name = self.request.form.get('reviewer')
1499
        reviewer = getUtility(IPersonSet).getByName(reviewer_name)
1500
        if (reviewer.is_team and self.user.inTeam(reviewer) and
1501
            self.users_vote_ref is None):
1502
            vote_ref = self.context.getUsersVoteReference(
1503
                reviewer, review_type)
1504
            if vote_ref is not None:
7055.6.30 by Paul Hummer
Added technical debt *sigh*
1505
                # Claim this vote reference, i.e. say that the individual
1506
                # self. user is doing this review ond behalf of the 'reviewer'
1507
                # team.
10147.3.3 by Tim Penhey
Fix the team claiming reviews.
1508
                vote_ref.claimReview(self.user)
7055.6.11 by Tim Penhey
Lots of changes to do with the request reviewer, voting and comment pages.
1509
11019.3.1 by Tim Penhey
Extract the DecoratedBug and DecorateBranch out of the browser/branch module and into its own. Use a DecoratedBranch for the new revisions.
1510
        self.context.createComment(
7055.6.11 by Tim Penhey
Lots of changes to do with the request reviewer, voting and comment pages.
1511
            self.user, subject=None, content=data['comment'],
1512
            vote=data['vote'], review_type=review_type)
1513
1514
    @property
1515
    def next_url(self):
1516
        """Always take the user back to the merge proposal itself."""
1517
        return canonical_url(self.context)
1518
1519
    cancel_url = next_url
9737.2.5 by Tim Penhey
Make an xhtml renderer adapter for the commit_message.
1520
1521
9691.6.10 by Tim Penhey
Make the diff load using the API rather than hitting the page.
1522
class FormatPreviewDiffView(LaunchpadView, DiffRenderingMixin):
1523
    """A simple view to render a diff formatted nicely."""
1524
1525
    @property
1526
    def preview_diff(self):
1527
        return self.context
1528
1529
1530
class PreviewDiffHTMLRepresentation:
1531
    adapts(IPreviewDiff, IWebServiceClientRequest)
1532
    implements(Interface)
1533
1534
    def __init__(self, diff, request):
1535
        self.diff = diff
1536
        self.request = request
1537
1538
    def __call__(self):
1539
        """Render `BugBranch` as XHTML using the webservice."""
1540
        diff_view = getMultiAdapter(
1541
            (self.diff, self.request), name="+diff")
1542
        return diff_view()