Merge lp:~abentley/launchpad/resubmit-change-branch into lp:launchpad

Proposed by Aaron Bentley
Status: Merged
Approved by: Aaron Bentley
Approved revision: no longer in the source branch.
Merged at revision: 11904
Proposed branch: lp:~abentley/launchpad/resubmit-change-branch
Merge into: lp:launchpad
Diff against target: 407 lines (+218/-31)
6 files modified
lib/lp/code/browser/branchmergeproposal.py (+37/-5)
lib/lp/code/browser/tests/test_branchmergeproposal.py (+94/-1)
lib/lp/code/interfaces/branchmergeproposal.py (+25/-11)
lib/lp/code/model/branchmergeproposal.py (+20/-8)
lib/lp/code/model/tests/test_branchmergeproposal.py (+38/-0)
lib/lp/code/templates/branchmergeproposal-resubmit.pt (+4/-6)
To merge this branch: bzr merge lp:~abentley/launchpad/resubmit-change-branch
Reviewer Review Type Date Requested Status
Paul Hummer (community) ui Approve
Brad Crittenden (community) code Approve
Review via email: mp+40126@code.launchpad.net

Commit message

Allow changing branches, description when resubmitting merge proposal.

Description of the change

= Summary =
Fix bug #504369: Resubmit should allow changing the branches
Screenshot: http://people.canonical.com/~abentley/resubmit-change-branch.png

== Proposed fix ==
Add fields for the branches to the resubmit page. Also allow changing the
description. Also allow the user to break the link between the old and new
proposals.

== Pre-implementation notes ==
None

== Implementation details ==
Changed the schema to ResubmitSchema to add the break_link boolean.
Changed from MergeProposalEditView to LaunchpadFormView to avoid attempting to
adapt BranchMergeProposal to ResubmitSchema.
Changed the text to reflect the fact that users now control the branches.
Changed the text to refer to the current merge proposal as "this proposal",
since the new one doesn't exist yet.

== Tests ==
bin/test -vt proposal code

== Demo and Q/A ==
Create a merge proposal. Resubmit it, changing all branches, description, and
selecting "break link". A new proposal should be created with the specified
branches and description, and with no link to the previous one. The previous
one should be marked "superseded".

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/code/templates/branchmergeproposal-resubmit.pt
  lib/lp/code/interfaces/branchmergeproposal.py
  lib/lp/code/model/branchmergeproposal.py
  lib/lp/code/model/tests/test_branchmergeproposal.py
  lib/lp/code/browser/tests/test_branchmergeproposal.py
  lib/lp/code/browser/branchmergeproposal.py

./lib/lp/code/browser/branchmergeproposal.py
     986: E202 whitespace before ']'
     988: E301 expected 1 blank line, found 0

To post a comment you must log in.
Revision history for this message
Brad Crittenden (bac) wrote :

Thanks for the very nice cover letter Aaron.

The lint issues both look real and need to be fixed. The list for field_names should be multi-line with one entry per line ending with a trailing comma.

In 'initial_values' the list 'values' is really key, value pairs so calling it 'items' would make more sense.

In 'test_resubmit_text' your use of string concatenation the call to 'assertTextMatches...' is hard to read. Using a variable like
  expected = '...'
would be more readable.

s/If this is the same as the target branch/
  If this branch is the same as the target branch

Please provide a 'cancel_url' so that you get a cancel link.

Finally, it is clear you thought it out since you test for it explicitly, but I question the need to create a new merge proposal when nothing has changed. Do you have a good use case for that?

This new functionality is a vast improvement. Thanks for recognizing the need and providing the solution.

review: Approve (code)
Revision history for this message
Aaron Bentley (abentley) wrote :

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

On 11/05/2010 01:42 AM, Brad Crittenden wrote:
> Finally, it is clear you thought it out since you test for it explicitly, but I question the need to create a new merge proposal when nothing has changed. Do you have a good use case for that?

Actually, that was the existing functionality. It is for cases where
the proposal is marked "resubmit", many changes are made, and finally
the proposal is resubmitted.

Aaron
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.10 (GNU/Linux)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org/

iEYEARECAAYFAkzUJpAACgkQ0F+nu1YWqI1WDwCdEKhJpAiuZXSzxf6/PK5gmzGO
wUUAnjmKdw60t5erpfaDAegkFkU22zuN
=houq
-----END PGP SIGNATURE-----

Revision history for this message
Paul Hummer (rockstar) wrote :

As discussed on the phone, I'm of the opinion that we should ensure that both the source and the target aren't changed. In cases like that, I think that we should encouraging people to create a whole new merge proposal. I've been outvoted in this case, but I thought I'd mention it in the MP just for history's sake.

review: Approve (ui)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/code/browser/branchmergeproposal.py'
--- lib/lp/code/browser/branchmergeproposal.py 2010-09-29 07:09:03 +0000
+++ lib/lp/code/browser/branchmergeproposal.py 2010-11-11 02:54:49 +0000
@@ -57,6 +57,7 @@
57 Interface,57 Interface,
58 )58 )
59from zope.schema import (59from zope.schema import (
60 Bool,
60 Choice,61 Choice,
61 Int,62 Int,
62 Text,63 Text,
@@ -965,20 +966,51 @@
965 % revision_name)966 % revision_name)
966967
967968
968class BranchMergeProposalResubmitView(MergeProposalEditView,969class ResubmitSchema(IBranchMergeProposal):
970
971 break_link = Bool(
972 title=u'Start afresh',
973 description=(
974 u'Do not show old conversation and do not link to superseded'
975 ' proposal.'),
976 default=False,)
977
978
979class BranchMergeProposalResubmitView(LaunchpadFormView,
969 UnmergedRevisionsMixin):980 UnmergedRevisionsMixin):
970 """The view to resubmit a proposal to merge."""981 """The view to resubmit a proposal to merge."""
971982
972 schema = IBranchMergeProposal983 schema = ResubmitSchema
984 for_input = True
973 page_title = label = "Resubmit proposal to merge"985 page_title = label = "Resubmit proposal to merge"
974 field_names = []986 field_names = [
987 'source_branch',
988 'target_branch',
989 'prerequisite_branch',
990 'description',
991 'break_link',
992 ]
993
994 def initialize(self):
995 self.cancel_url = canonical_url(self.context)
996 super(BranchMergeProposalResubmitView, self).initialize()
997
998 @property
999 def initial_values(self):
1000 UNSET = object()
1001 items = ((key, getattr(self.context, key, UNSET)) for key in
1002 self.field_names if key != 'break_link')
1003 return dict(item for item in items if item[1] is not UNSET)
9751004
976 @action('Resubmit', name='resubmit')1005 @action('Resubmit', name='resubmit')
977 @update_and_notify
978 def resubmit_action(self, action, data):1006 def resubmit_action(self, action, data):
979 """Resubmit this proposal."""1007 """Resubmit this proposal."""
980 proposal = self.context.resubmit(self.user)1008 proposal = self.context.resubmit(
1009 self.user, data['source_branch'], data['target_branch'],
1010 data['prerequisite_branch'], data['description'],
1011 data['break_link'])
981 self.next_url = canonical_url(proposal)1012 self.next_url = canonical_url(proposal)
1013 return proposal
9821014
9831015
984class BranchMergeProposalEditView(MergeProposalEditView):1016class BranchMergeProposalEditView(MergeProposalEditView):
9851017
=== modified file 'lib/lp/code/browser/tests/test_branchmergeproposal.py'
--- lib/lp/code/browser/tests/test_branchmergeproposal.py 2010-10-25 05:33:20 +0000
+++ lib/lp/code/browser/tests/test_branchmergeproposal.py 2010-11-11 02:54:49 +0000
@@ -34,6 +34,7 @@
34 BranchMergeProposalChangeStatusView,34 BranchMergeProposalChangeStatusView,
35 BranchMergeProposalContextMenu,35 BranchMergeProposalContextMenu,
36 BranchMergeProposalMergedView,36 BranchMergeProposalMergedView,
37 BranchMergeProposalResubmitView,
37 BranchMergeProposalVoteView,38 BranchMergeProposalVoteView,
38 DecoratedCodeReviewVoteReference,39 DecoratedCodeReviewVoteReference,
39 ICodeReviewNewRevisions,40 ICodeReviewNewRevisions,
@@ -53,7 +54,9 @@
53 make_merge_proposal_without_reviewers,54 make_merge_proposal_without_reviewers,
54 )55 )
55from lp.testing import (56from lp.testing import (
57 BrowserTestCase,
56 login_person,58 login_person,
59 person_logged_in,
57 TestCaseWithFactory,60 TestCaseWithFactory,
58 time_counter,61 time_counter,
59 )62 )
@@ -501,7 +504,7 @@
501 self.assertOnePendingReview(proposal, reviewer)504 self.assertOnePendingReview(proposal, reviewer)
502 self.assertIs(None, proposal.description)505 self.assertIs(None, proposal.description)
503506
504 def test_register_request_review_type(self):507 def test_register_request_review_type_branch_reviewer(self):
505 # We can ask for a specific review type. The target branch has a508 # We can ask for a specific review type. The target branch has a
506 # reviewer so that reviewer should be used.509 # reviewer so that reviewer should be used.
507 target_branch, reviewer = self._makeTargetBranchWithReviewer()510 target_branch, reviewer = self._makeTargetBranchWithReviewer()
@@ -515,6 +518,96 @@
515 self.assertIs(None, proposal.description)518 self.assertIs(None, proposal.description)
516519
517520
521class TestBranchMergeProposalResubmitView(TestCaseWithFactory):
522 """Test BranchMergeProposalResubmitView."""
523
524 layer = DatabaseFunctionalLayer
525
526 def createView(self):
527 """Create the required view."""
528 context = self.factory.makeBranchMergeProposal()
529 self.useContext(person_logged_in(context.registrant))
530 view = BranchMergeProposalResubmitView(
531 context, LaunchpadTestRequest())
532 view.initialize()
533 return view
534
535 def test_resubmit_action(self):
536 """resubmit_action resubmits the proposal."""
537 view = self.createView()
538 context = view.context
539 new_proposal = view.resubmit_action.success(
540 {'source_branch': context.source_branch,
541 'target_branch': context.target_branch,
542 'prerequisite_branch': context.prerequisite_branch,
543 'description': None,
544 'break_link': False,
545 })
546 self.assertEqual(new_proposal.supersedes, context)
547 self.assertEqual(new_proposal.source_branch, context.source_branch)
548 self.assertEqual(new_proposal.target_branch, context.target_branch)
549 self.assertEqual(
550 new_proposal.prerequisite_branch, context.prerequisite_branch)
551
552 def test_resubmit_action_change_branches(self):
553 """Changing the branches changes the branches in the new proposal."""
554 view = self.createView()
555 target = view.context.source_branch.target
556 new_source = self.factory.makeBranchTargetBranch(target)
557 new_target = self.factory.makeBranchTargetBranch(target)
558 new_prerequisite = self.factory.makeBranchTargetBranch(target)
559 new_proposal = view.resubmit_action.success(
560 {'source_branch': new_source, 'target_branch': new_target,
561 'prerequisite_branch': new_prerequisite,
562 'description': 'description',
563 'break_link': False,
564 })
565 self.assertEqual(new_proposal.supersedes, view.context)
566 self.assertEqual(new_proposal.source_branch, new_source)
567 self.assertEqual(new_proposal.target_branch, new_target)
568 self.assertEqual(new_proposal.prerequisite_branch, new_prerequisite)
569
570 def test_resubmit_action_break_link(self):
571 """Enabling break_link prevents linking the old and new proposals."""
572 view = self.createView()
573 context = view.context
574 new_proposal = view.resubmit_action.success(
575 {'source_branch': context.source_branch,
576 'target_branch': context.target_branch,
577 'prerequisite_branch': context.prerequisite_branch,
578 'description': None,
579 'break_link': True,
580 })
581 self.assertIs(None, new_proposal.supersedes)
582
583
584class TestResubmitBrowser(BrowserTestCase):
585 """Browser tests for resubmitting branch merge proposals."""
586
587 layer = DatabaseFunctionalLayer
588
589 def test_resubmit_text(self):
590 """The text of the resubmit page is as expected."""
591 bmp = self.factory.makeBranchMergeProposal(registrant=self.user)
592 text = self.getMainText(bmp, '+resubmit')
593 expected = (
594 'Resubmit proposal to merge.*'
595 'Source Branch:.*'
596 'Target Branch:.*'
597 'Prerequisite Branch:.*'
598 'Description.*'
599 'Start afresh.*')
600 self.assertTextMatchesExpressionIgnoreWhitespace(expected, text)
601
602 def test_resubmit_controls(self):
603 """Proposals can be resubmitted using the browser."""
604 bmp = self.factory.makeBranchMergeProposal(registrant=self.user)
605 browser = self.getViewBrowser(bmp, '+resubmit')
606 browser.getControl('Description').value = 'flibble'
607 browser.getControl('Resubmit').click()
608 self.assertEqual('flibble', bmp.superseded_by.description)
609
610
518class TestBranchMergeProposalView(TestCaseWithFactory):611class TestBranchMergeProposalView(TestCaseWithFactory):
519612
520 layer = LaunchpadFunctionalLayer613 layer = LaunchpadFunctionalLayer
521614
=== modified file 'lib/lp/code/interfaces/branchmergeproposal.py'
--- lib/lp/code/interfaces/branchmergeproposal.py 2010-10-25 05:33:20 +0000
+++ lib/lp/code/interfaces/branchmergeproposal.py 2010-11-11 02:54:49 +0000
@@ -45,6 +45,7 @@
45from lazr.restful.fields import (45from lazr.restful.fields import (
46 CollectionField,46 CollectionField,
47 Reference,47 Reference,
48 ReferenceChoice,
48 )49 )
49from zope.event import notify50from zope.event import notify
50from zope.interface import (51from zope.interface import (
@@ -62,6 +63,7 @@
62 TextLine,63 TextLine,
63 )64 )
6465
66from canonical.database.constants import DEFAULT
65from canonical.launchpad import _67from canonical.launchpad import _
66from canonical.launchpad.interfaces.launchpad import IPrivacy68from canonical.launchpad.interfaces.launchpad import IPrivacy
67from canonical.launchpad.webapp.interfaces import ITableBatchNavigator69from canonical.launchpad.webapp.interfaces import ITableBatchNavigator
@@ -112,25 +114,26 @@
112 description=_('The person who registered the landing target.')))114 description=_('The person who registered the landing target.')))
113115
114 source_branch = exported(116 source_branch = exported(
115 Reference(117 ReferenceChoice(
116 title=_('Source Branch'), schema=IBranch,118 title=_('Source Branch'), schema=IBranch, vocabulary='Branch',
117 required=True, readonly=True,119 required=True, readonly=True,
118 description=_("The branch that has code to land.")))120 description=_("The branch that has code to land.")))
119121
120 target_branch = exported(122 target_branch = exported(
121 Reference(123 ReferenceChoice(
122 title=_('Target Branch'),124 title=_('Target Branch'),
123 schema=IBranch, required=True, readonly=True,125 schema=IBranch, vocabulary='Branch', required=True, readonly=True,
124 description=_(126 description=_(
125 "The branch that the source branch will be merged into.")))127 "The branch that the source branch will be merged into.")))
126128
127 prerequisite_branch = exported(129 prerequisite_branch = exported(
128 Reference(130 ReferenceChoice(
129 title=_('Dependent Branch'),131 title=_('Prerequisite Branch'),
130 schema=IBranch, required=False, readonly=True,132 schema=IBranch, vocabulary='Branch', required=False,
131 description=_("The branch that the source branch branched from. "133 readonly=True, description=_(
132 "If this is the same as the target branch, then "134 "The branch that the source branch branched from. "
133 "leave this field blank.")))135 "If this branch is the same as the target branch, then "
136 "leave this field blank.")))
134137
135 # This is redefined from IPrivacy.private because the attribute is138 # This is redefined from IPrivacy.private because the attribute is
136 # read-only. The value is determined by the involved branches.139 # read-only. The value is determined by the involved branches.
@@ -430,13 +433,24 @@
430 :type merge_reporter: ``Person``433 :type merge_reporter: ``Person``
431 """434 """
432435
433 def resubmit(registrant):436 def resubmit(registrant, source_branch=None, target_branch=None,
437 prerequisite_branch=DEFAULT):
434 """Mark the branch merge proposal as superseded and return a new one.438 """Mark the branch merge proposal as superseded and return a new one.
435439
436 The new proposal is created as work-in-progress, and copies across440 The new proposal is created as work-in-progress, and copies across
437 user-entered data like the whiteboard. All the current proposal's441 user-entered data like the whiteboard. All the current proposal's
438 reviewers, including those who have only been nominated, are requested442 reviewers, including those who have only been nominated, are requested
439 to review the new proposal.443 to review the new proposal.
444
445 :param registrant: The person registering the new proposal.
446 :param source_branch: The source_branch for the new proposal (defaults
447 to the current source_branch).
448 :param target_branch: The target_branch for the new proposal (defaults
449 to the current target_branch).
450 :param prerequisite_branch: The prerequisite_branch for the new
451 proposal (defaults to the current prerequisite_branch).
452 :param description: The description for the new proposal (defaults to
453 the current description).
440 """454 """
441455
442 def isMergable():456 def isMergable():
443457
=== modified file 'lib/lp/code/model/branchmergeproposal.py'
--- lib/lp/code/model/branchmergeproposal.py 2010-10-19 00:44:24 +0000
+++ lib/lp/code/model/branchmergeproposal.py 2010-11-11 02:54:49 +0000
@@ -543,8 +543,19 @@
543 date_merged = UTC_NOW543 date_merged = UTC_NOW
544 self.date_merged = date_merged544 self.date_merged = date_merged
545545
546 def resubmit(self, registrant):546 def resubmit(self, registrant, source_branch=None, target_branch=None,
547 prerequisite_branch=DEFAULT, description=None,
548 break_link=False):
547 """See `IBranchMergeProposal`."""549 """See `IBranchMergeProposal`."""
550 if source_branch is None:
551 source_branch = self.source_branch
552 if target_branch is None:
553 target_branch = self.target_branch
554 # DEFAULT instead of None, because None is a valid value.
555 if prerequisite_branch is DEFAULT:
556 prerequisite_branch = self.prerequisite_branch
557 if description is None:
558 description = self.description
548 # You can transition from REJECTED to SUPERSEDED, but559 # You can transition from REJECTED to SUPERSEDED, but
549 # not from MERGED or SUPERSEDED.560 # not from MERGED or SUPERSEDED.
550 self._transitionToState(561 self._transitionToState(
@@ -553,15 +564,16 @@
553 # a database query to identify if there are any active proposals564 # a database query to identify if there are any active proposals
554 # with the same source and target branches.565 # with the same source and target branches.
555 self.syncUpdate()566 self.syncUpdate()
556 review_requests = set(567 review_requests = list(set(
557 (vote.reviewer, vote.review_type) for vote in self.votes)568 (vote.reviewer, vote.review_type) for vote in self.votes))
558 proposal = self.source_branch.addLandingTarget(569 proposal = source_branch.addLandingTarget(
559 registrant=registrant,570 registrant=registrant,
560 target_branch=self.target_branch,571 target_branch=target_branch,
561 prerequisite_branch=self.prerequisite_branch,572 prerequisite_branch=prerequisite_branch,
562 description=self.description,573 description=description,
563 needs_review=True, review_requests=review_requests)574 needs_review=True, review_requests=review_requests)
564 self.superseded_by = proposal575 if not break_link:
576 self.superseded_by = proposal
565 # This sync update is needed to ensure that the transitive577 # This sync update is needed to ensure that the transitive
566 # properties of supersedes and superseded_by are visible to578 # properties of supersedes and superseded_by are visible to
567 # the old and the new proposal.579 # the old and the new proposal.
568580
=== modified file 'lib/lp/code/model/tests/test_branchmergeproposal.py'
--- lib/lp/code/model/tests/test_branchmergeproposal.py 2010-11-03 04:27:13 +0000
+++ lib/lp/code/model/tests/test_branchmergeproposal.py 2010-11-11 02:54:49 +0000
@@ -1659,6 +1659,9 @@
1659 bmp2.queue_status, BranchMergeProposalStatus.NEEDS_REVIEW)1659 bmp2.queue_status, BranchMergeProposalStatus.NEEDS_REVIEW)
1660 self.assertEqual(1660 self.assertEqual(
1661 bmp2, bmp1.superseded_by)1661 bmp2, bmp1.superseded_by)
1662 self.assertEqual(bmp1.source_branch, bmp2.source_branch)
1663 self.assertEqual(bmp1.target_branch, bmp2.target_branch)
1664 self.assertEqual(bmp1.prerequisite_branch, bmp2.prerequisite_branch)
16621665
1663 def test_resubmit_re_requests_review(self):1666 def test_resubmit_re_requests_review(self):
1664 """Resubmit should request new reviews.1667 """Resubmit should request new reviews.
@@ -1680,6 +1683,41 @@
1680 (reviewer, 'specious')]),1683 (reviewer, 'specious')]),
1681 set((vote.reviewer, vote.review_type) for vote in bmp2.votes))1684 set((vote.reviewer, vote.review_type) for vote in bmp2.votes))
16821685
1686 def test_resubmit_no_reviewers(self):
1687 """Resubmitting a proposal with no reviewers should work."""
1688 bmp = make_merge_proposal_without_reviewers(self.factory)
1689 with person_logged_in(bmp.registrant):
1690 bmp2 = bmp.resubmit(bmp.registrant)
1691
1692 def test_resubmit_changes_branches(self):
1693 """Resubmit changes branches, if specified."""
1694 original = self.factory.makeBranchMergeProposal()
1695 self.useContext(person_logged_in(original.registrant))
1696 branch_target = original.source_branch.target
1697 new_source = self.factory.makeBranchTargetBranch(branch_target)
1698 new_target = self.factory.makeBranchTargetBranch(branch_target)
1699 new_prerequisite = self.factory.makeBranchTargetBranch(branch_target)
1700 revised = original.resubmit(original.registrant, new_source,
1701 new_target, new_prerequisite)
1702 self.assertEqual(new_source, revised.source_branch)
1703 self.assertEqual(new_target, revised.target_branch)
1704 self.assertEqual(new_prerequisite, revised.prerequisite_branch)
1705
1706 def test_resubmit_changes_description(self):
1707 """Resubmit changes description, if specified."""
1708 original = self.factory.makeBranchMergeProposal()
1709 self.useContext(person_logged_in(original.registrant))
1710 revised = original.resubmit(original.registrant, description='foo')
1711 self.assertEqual('foo', revised.description)
1712
1713 def test_resubmit_breaks_link(self):
1714 """Resubmit breaks link, if specified."""
1715 original = self.factory.makeBranchMergeProposal()
1716 self.useContext(person_logged_in(original.registrant))
1717 revised = original.resubmit(
1718 original.registrant, break_link=True)
1719 self.assertIs(None, original.superseded_by)
1720
16831721
1684class TestCreateMergeProposalJob(TestCaseWithFactory):1722class TestCreateMergeProposalJob(TestCaseWithFactory):
1685 """Tests for CreateMergeProposalJob."""1723 """Tests for CreateMergeProposalJob."""
16861724
=== modified file 'lib/lp/code/templates/branchmergeproposal-resubmit.pt'
--- lib/lp/code/templates/branchmergeproposal-resubmit.pt 2009-10-29 13:58:43 +0000
+++ lib/lp/code/templates/branchmergeproposal-resubmit.pt 2010-11-11 02:54:49 +0000
@@ -14,14 +14,12 @@
14 <div metal:fill-slot="extra_info">14 <div metal:fill-slot="extra_info">
15 <p>15 <p>
16 Resubmitting this proposal to merge will cause this proposal to be16 Resubmitting this proposal to merge will cause this proposal to be
17 marked as <strong>superseded</strong>. Another merge proposal will17 marked as <strong>superseded</strong>. A new proposal will
18 be created, with the same source, target and prerequisite branch18 be created.
19 (if any).
20 </p>19 </p>
21 <p>20 <p>
22 Everyone who has reviewed the previous proposal or was requested to21 Everyone who has reviewed this proposal or was requested to review this
23 review the previous proposal will be requested to review the new22 proposal will be requested to review the new proposal.
24 proposal.
25 </p>23 </p>
26 </div>24 </div>
27 </div>25 </div>