Merge lp:~wallyworld/launchpad/delete-bugtask-ui-878909 into lp:launchpad

Proposed by Ian Booth
Status: Merged
Approved by: Curtis Hovey
Approved revision: no longer in the source branch.
Merged at revision: 14255
Proposed branch: lp:~wallyworld/launchpad/delete-bugtask-ui-878909
Merge into: lp:launchpad
Diff against target: 345 lines (+199/-3)
8 files modified
lib/lp/app/javascript/activator/assets/skins/sam/activator-skin.css (+1/-0)
lib/lp/bugs/browser/bugtask.py (+35/-1)
lib/lp/bugs/browser/configure.zcml (+6/-0)
lib/lp/bugs/browser/tests/test_bugtask.py (+118/-0)
lib/lp/bugs/configure.zcml (+1/-0)
lib/lp/bugs/templates/bugtask-delete.pt (+26/-0)
lib/lp/bugs/templates/bugtask-tasks-and-nominations-table-row.pt (+10/-0)
lib/lp/testing/views.py (+2/-2)
To merge this branch: bzr merge lp:~wallyworld/launchpad/delete-bugtask-ui-878909
Reviewer Review Type Date Requested Status
Curtis Hovey (community) code Approve
Review via email: mp+80179@code.launchpad.net

Commit message

[r=sinzui][bug=878909] Update the ui to support bugtask deletion.

Description of the change

Update the ui to support bugtask deletion.

== Implementation ==

Add a new BugTaskDeleteView and render delete icons next to each bug task that can be deleted by the logged in user.
This mp performs the delete using html forms. A subsequent branch will provide ajax support.

The permission checking code was moved from the security adaptor to a method on BugTask so it could be reused by the view.
To recap: the delete capability is protected by a feature flag, is restricted to certain roles, and the last bug tasks for a bug cannot be deleted.

A change was made to the lazr-js activator css to ensure the edit icon lines up properly.

== Demo and QA ==

The delete icons are rendered as shown in the screenshot.

http://people.canonical.com/~ianb/bugtask-delete-icons.png

== Tests ==

Add tests to browser/tests/test_bugtask:

TestBugTaskDeleteLinks
(test that the delete links/icons are rendered as expected)

TestBugTaskDeleteView
(test that the delete view itself works as expected)

== Lint ==

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/app/javascript/activator/assets/skins/sam/activator-skin.css
  lib/lp/bugs/configure.zcml
  lib/lp/bugs/security.py
  lib/lp/bugs/browser/bugtask.py
  lib/lp/bugs/browser/configure.zcml
  lib/lp/bugs/browser/tests/test_bugtask.py
  lib/lp/bugs/interfaces/bugtask.py
  lib/lp/bugs/model/bugtask.py
  lib/lp/bugs/templates/bugtask-delete.pt
  lib/lp/bugs/templates/bugtask-tasks-and-nominations-table-row.pt

To post a comment you must log in.
Revision history for this message
Curtis Hovey (sinzui) wrote :
Download full text (10.7 KiB)

Hi Ian.

I have a few concerns about the safety and maintainability of of two changes. I have some suggestions and questions too.

> === modified file 'lib/lp/bugs/browser/bugtask.py'
> --- lib/lp/bugs/browser/bugtask.py 2011-10-19 14:54:07 +0000
> +++ lib/lp/bugs/browser/bugtask.py 2011-10-25 08:17:26 +0000
> @@ -1757,6 +1758,38 @@
> self.updateContextFromData(data)
>
>
> +class BugTaskDeletionView(LaunchpadFormView):
> + """Used to delete a bugtask."""
> +
> + schema = IBugTask
> + field_names = []
> +
> + label = 'Remove bug task'
> + page_title = label
> +
> + @property
> + def confirmation_message(self):
> + return ('<p>You are about to mark bug %s<br>'
> + 'as no longer affecting %s.</p>'
> + '<p>This operation will be permanent and cannot be '
> + 'undone.</p>'
> + % (self.context.bug.title,
> + self.context.target.bugtargetdisplayname))

This is dangerous. Both title and displayname may *always* contain
markup. We use structured() to build safe interpolated messages.

Would direct language be more effective at conveying the consequences of the
action:
    Deletion is permanent, it cannot be undone.

...

> @@ -3552,6 +3585,8 @@
> row_css_class='highlight' if is_primary else None,
> target_link=canonical_url(self.context.target),
> target_link_title=self.target_link_title,
> + user_can_delete=self.user_can_delete_bugtask,
> + delete_link=link + '/+delete',

While I am certain this link will work, but it is a crafted link we need to
maintain with extra testing.
    canonical_url(self.context.target, view_name='+delete')
will raise an exception if the named view is unregistered.

> === modified file 'lib/lp/bugs/browser/tests/test_bugtask.py'
> --- lib/lp/bugs/browser/tests/test_bugtask.py 2011-10-05 18:02:45 +0000
> +++ lib/lp/bugs/browser/tests/test_bugtask.py 2011-10-25 08:17:26 +0000
> @@ -606,6 +614,99 @@
> self.assertIn(series.product.displayname, content)
>
>
> +class TestBugTaskDeleteLinks(TestCaseWithFactory):
> + """ Test that the delete icons/links for each relevant bug task are
> + correctly rendered.
> +
> + Bug task deletion is protected by a feature flag.
> + """

The first line may not wrap according to PEP 256.

+ layer = DatabaseFunctionalLayer
+
+ def test_cannot_delete_only_bugtask(self):
+ # The last bugtask cannot be deleted.
+ bug = self.factory.makeBug()
+ login_person(bug.owner)
+ view = create_initialized_view(
+ bug, name='+bugtasks-and-nominations-table')
+ row_view = view._getTableRowView(bug.default_bugtask, False, False)
+ self.assertFalse(row_view.user_can_delete_bugtask)
+ del get_property_cache(row_view).user_can_delete_bugtask
+ with FeatureFixture(DELETE_BUGTASK_ENABLED):
+ self.assertFalse(row_view.user_can_delete_bugtask)
+
+ def test_can_delete_bugtask_if_authorised(self):
+ # The bugtask can be deleted if the user if authorised.
+ bug = self.factory.makeBug()
+ bugtask = self.fac...

review: Needs Fixing (code)
Revision history for this message
Ian Booth (wallyworld) wrote :
Download full text (4.6 KiB)

Hi

Thanks for the excellent review.

>
> This is dangerous. Both title and displayname may *always* contain
> markup. We use structured() to build safe interpolated messages.
>

I originally tried using structured() but the legit html in the message
got escaped. I suspect I may have messed up using the api and had meant
to come back to look at it again but forgot. I'll take another look.

> Would direct language be more effective at conveying the consequences of the
> action:
> Deletion is permanent, it cannot be undone.
>

Sure.

>> + user_can_delete=self.user_can_delete_bugtask,
>> + delete_link=link + '/+delete',
>
> While I am certain this link will work, but it is a crafted link we need to
> maintain with extra testing.
> canonical_url(self.context.target, view_name='+delete')
> will raise an exception if the named view is unregistered.
>

Yes, much better, thanks.

>>
>> +class TestBugTaskDeleteLinks(TestCaseWithFactory):
>> + """ Test that the delete icons/links for each relevant bug task are
>> + correctly rendered.
>> +
>> + Bug task deletion is protected by a feature flag.
>> + """
>
> The first line may not wrap according to PEP 256.
>

Yeah, sometimes it is hard to limit the text to only one line and still
have it make sense. I'll truncate.

>
> The TestBrowser is an indirect means to test rendering. The view does that.
> you can render any view (that had the principal argument passed) by calling
> it; view(). Launchpad views add striping rules so __call__ calls render().
> You can render a view's templates using view() or view.render(). It is much
> faster than TestBrowser.
>

I only resorted to using TestBrowser because I could get view.render()
to work for this case. It failed with various errors (eg None broken the
chain etc) depending on what I tried. I see you suggest:

 > view = create_initialized_view(
 > bug, name='+bugtasks-and-nominations-table'
 > principal=bugtask.owner)

I tried things like eg:

  view = create_initialized_view(bugtask, name='+index',
     rootsite='bugs', principal=bugtask.owner)

  view = create_initialized_view(bug, name='+index',
     rootsite='bugs', principal=bugtask.owner)

I'd like to understand why view.render() failed.

>
> And I see you did you view.render() here. I do not care for exact testing
> of user text because it tends to change often and get tested more than once.
> I put ids on the text fragment, id="deletion-waring-message", and just test
> for the presence. This permits non technical users to make trivial changes
> to the text while the code ensures that the intent of the message is still
> in the content.
>

Yes, good idea. I had doubts how far we needed to go here, hence the use
of the regex to test the gist of the message.

>
> This code is repeating the mistakes of Bugs past. The code embeds roles
> and celebrities into model code. This is the primary reason bug permissions
> are fucked up. I believe this logic belongs in lp.bugs.security, where I
> think this came from. userCanDelete() could do this:
> def userCanDelete(self):
> return check_permi...

Read more...

Revision history for this message
William Grant (wgrant) wrote :

37 + @property
38 + def confirmation_message(self):
39 + return ('<p>You are about to mark bug %s<br>'
40 + 'as no longer affecting %s.</p>'
41 + '<p>This operation will be permanent and cannot be '
42 + 'undone.</p>'
43 + % (self.context.bug.title,
44 + self.context.target.bugtargetdisplayname))

As Curtis says, this is a security hole. Why is this not done in the template? Also, the operation is not permanent -- it can be undone, by readding the task.

Revision history for this message
Ian Booth (wallyworld) wrote :

On 26/10/11 08:36, William Grant wrote:
> 37 + @property
> 38 + def confirmation_message(self):
> 39 + return ('<p>You are about to mark bug %s<br>'
> 40 + 'as no longer affecting %s.</p>'
> 41 + '<p>This operation will be permanent and cannot be '
> 42 + 'undone.</p>'
> 43 + % (self.context.bug.title,
> 44 + self.context.target.bugtargetdisplayname))
>
> As Curtis says, this is a security hole. Why is this not done in the template? Also, the operation is not permanent -- it can be undone, by readding the task.

I'm fixing the hole.
The message is worded as per the bug report. I think the indent is that
in general, deletion is a serious decision and the user must be sure.

Revision history for this message
Ian Booth (wallyworld) wrote :

Hi

I've addressed the issues raised. Reverting the security check code to
how it was makes the diff much shorter.

>> @@ -3552,6 +3585,8 @@
>> row_css_class='highlight' if is_primary else None,
>> target_link=canonical_url(self.context.target),
>> target_link_title=self.target_link_title,
>> + user_can_delete=self.user_can_delete_bugtask,
>> + delete_link=link + '/+delete',
>
> While I am certain this link will work, but it is a crafted link we need to
> maintain with extra testing.
> canonical_url(self.context.target, view_name='+delete')
> will raise an exception if the named view is unregistered.
>

I copied what was done in the test for the +editstatus link. I've fixed
that one too.

>
> The TestBrowser is an indirect means to test rendering. The view does that.
> you can render any view (that had the principal argument passed) by calling
> it; view(). Launchpad views add striping rules so __call__ calls render().
> You can render a view's templates using view() or view.render(). It is much
> faster than TestBrowser.
>
> def test_bugtask_delete_icon(self):
> # The bugtask delete icon is rendered correctly for those tasks the
> # user is allowed to delete.
> bug = self.factory.makeBug()
> bugtask = self.factory.makeBugTask(bug=bug)
> with FeatureFixture(DELETE_BUGTASK_ENABLED):
> login_person(bugtask.owner)
> view = create_initialized_view(
> bug, name='+bugtasks-and-nominations-table'
> principal=bugtask.owner)
> content = view.render()
> # bugtask can be deleted because the user owns it.
> delete_icon = find_tag_by_id(
> content, 'bugtask-delete-task%d' % bugtask.id)
> self.assertEqual(url + '/+delete', delete_icon['href'])
> # default_bugtask cannot be deleted.
> delete_icon = find_tag_by_id(
> content, 'bugtask-delete-task%d' % bug.default_bugtask.id)
> self.assertIsNone(delete_icon)
>

I double checked my original work. I tried again using
 > view = create_initialized_view(
 > bug, name='+bugtasks-and-nominations-table'
 > principal=bugtask.ow

and it wouldn't render. I also tried other variations. So I think
TestBrowser is required for this test sadly.

Revision history for this message
Ian Booth (wallyworld) wrote :

I figured out how to avoid using TestBrowser. One key step was to shove
the default bugtask into LaunchBag. Then the view for each table row had
to be constructed and rendered separately. Obvious!

>
> The TestBrowser is an indirect means to test rendering. The view does that.
> you can render any view (that had the principal argument passed) by calling
> it; view(). Launchpad views add striping rules so __call__ calls render().
> You can render a view's templates using view() or view.render(). It is much
> faster than TestBrowser.

<new stuff>
             login_person(bugtask.owner)
             getUtility(ILaunchBag).add(bug.default_bugtask)
             view = create_initialized_view(
                 bug, name='+bugtasks-and-nominations-table',
                 principal=bugtask.owner)
             # We render the bug task table rows - there are 2 bug tasks.
             subviews = view.getBugTaskAndNominationViews()
             self.assertEqual(2, len(subviews))
             default_bugtask_contents = subviews[0]()
             bugtask_contents = subviews[1]()
</new stuff>
             # bugtask can be deleted because the user owns it.
             delete_icon = find_tag_by_id(
                 bugtask_contents, 'bugtask-delete-task%d' % bugtask.id)
             delete_url = canonical_url(
                 bugtask, rootsite='bugs', view_name='+delete')
             self.assertEqual(delete_url, delete_icon['href'])
             # default_bugtask cannot be deleted.
             delete_icon = find_tag_by_id(
                 default_bugtask_contents,
                 'bugtask-delete-task%d' % bug.default_bugtask.id)
             self.assertIsNone(delete_icon)

Revision history for this message
Curtis Hovey (sinzui) wrote :

Thank you for fixing this Ian. I wish I remembered to my troubled writing tests for bug nominations where I too discovered the misdirection with the LaunchpadBag. While LaunchpadView, and most proper Zope views requires a context (bugtask) and a request passed on __init__, the very old bug and bugtask views did not use that. We might be able to remove the LaunchpadBag hacks if all the views are LaunchpadViews.

This looks good to land.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/app/javascript/activator/assets/skins/sam/activator-skin.css'
--- lib/lp/app/javascript/activator/assets/skins/sam/activator-skin.css 2011-06-29 14:56:15 +0000
+++ lib/lp/app/javascript/activator/assets/skins/sam/activator-skin.css 2011-11-01 00:49:29 +0000
@@ -2,6 +2,7 @@
22
3.yui3-skin-sam button.yui3-activator-act {3.yui3-skin-sam button.yui3-activator-act {
4 background: url('edit.png') 0 0 no-repeat;4 background: url('edit.png') 0 0 no-repeat;
5 vertical-align: middle;
5}6}
67
7.yui3-skin-sam .yui3-activator-processing button.yui3-activator-act {8.yui3-skin-sam .yui3-activator-processing button.yui3-activator-act {
89
=== modified file 'lib/lp/bugs/browser/bugtask.py'
--- lib/lp/bugs/browser/bugtask.py 2011-10-31 15:28:28 +0000
+++ lib/lp/bugs/browser/bugtask.py 2011-11-01 00:49:29 +0000
@@ -18,6 +18,7 @@
18 'BugTaskBreadcrumb',18 'BugTaskBreadcrumb',
19 'BugTaskContextMenu',19 'BugTaskContextMenu',
20 'BugTaskCreateQuestionView',20 'BugTaskCreateQuestionView',
21 'BugTaskDeletionView',
21 'BugTaskEditView',22 'BugTaskEditView',
22 'BugTaskExpirableListingView',23 'BugTaskExpirableListingView',
23 'BugTaskListingItem',24 'BugTaskListingItem',
@@ -159,6 +160,7 @@
159 custom_widget,160 custom_widget,
160 LaunchpadEditFormView,161 LaunchpadEditFormView,
161 LaunchpadFormView,162 LaunchpadFormView,
163 ReturnToReferrerMixin,
162 )164 )
163from lp.app.browser.lazrjs import (165from lp.app.browser.lazrjs import (
164 TextAreaEditorWidget,166 TextAreaEditorWidget,
@@ -1760,6 +1762,24 @@
1760 self.updateContextFromData(data)1762 self.updateContextFromData(data)
17611763
17621764
1765class BugTaskDeletionView(ReturnToReferrerMixin, LaunchpadFormView):
1766 """Used to delete a bugtask."""
1767
1768 schema = IBugTask
1769 field_names = []
1770
1771 label = 'Remove bug task'
1772 page_title = label
1773
1774 @action('Delete', name='delete_bugtask')
1775 def delete_bugtask_action(self, action, data):
1776 bugtask = self.context
1777 message = ("This bug no longer affects %s."
1778 % bugtask.target.bugtargetdisplayname)
1779 bugtask.delete()
1780 self.request.response.addNotification(message)
1781
1782
1763class BugTaskListingView(LaunchpadView):1783class BugTaskListingView(LaunchpadView):
1764 """A view designed for displaying bug tasks in lists."""1784 """A view designed for displaying bug tasks in lists."""
1765 # Note that this right now is only used in tests and to render1785 # Note that this right now is only used in tests and to render
@@ -3646,7 +3666,9 @@
3646 def initialize(self):3666 def initialize(self):
3647 super(BugTaskTableRowView, self).initialize()3667 super(BugTaskTableRowView, self).initialize()
3648 link = canonical_url(self.context)3668 link = canonical_url(self.context)
3649 task_link = edit_link = link + '/+editstatus'3669 task_link = edit_link = canonical_url(
3670 self.context, view_name='+editstatus')
3671 delete_link = canonical_url(self.context, view_name='+delete')
3650 can_edit = check_permission('launchpad.Edit', self.context)3672 can_edit = check_permission('launchpad.Edit', self.context)
3651 bugtask_id = self.context.id3673 bugtask_id = self.context.id
3652 launchbag = getUtility(ILaunchBag)3674 launchbag = getUtility(ILaunchBag)
@@ -3668,6 +3690,8 @@
3668 row_css_class='highlight' if is_primary else None,3690 row_css_class='highlight' if is_primary else None,
3669 target_link=canonical_url(self.context.target),3691 target_link=canonical_url(self.context.target),
3670 target_link_title=self.target_link_title,3692 target_link_title=self.target_link_title,
3693 user_can_delete=self.user_can_delete_bugtask,
3694 delete_link=delete_link,
3671 user_can_edit_importance=self.context.userCanEditImportance(3695 user_can_edit_importance=self.context.userCanEditImportance(
3672 self.user),3696 self.user),
3673 importance_css_class='importance' + self.context.importance.name,3697 importance_css_class='importance' + self.context.importance.name,
@@ -3817,6 +3841,16 @@
3817 """3841 """
3818 return self.context.userCanEditMilestone(self.user)3842 return self.context.userCanEditMilestone(self.user)
38193843
3844 @cachedproperty
3845 def user_can_delete_bugtask(self):
3846 """Can the user delete the bug task?
3847
3848 If yes, return True, otherwise return False.
3849 """
3850 bugtask = self.context
3851 return (check_permission('launchpad.Delete', bugtask)
3852 and bugtask.canBeDeleted())
3853
3820 @property3854 @property
3821 def style_for_add_milestone(self):3855 def style_for_add_milestone(self):
3822 if self.context.milestone is None:3856 if self.context.milestone is None:
38233857
=== modified file 'lib/lp/bugs/browser/configure.zcml'
--- lib/lp/bugs/browser/configure.zcml 2011-10-03 07:49:31 +0000
+++ lib/lp/bugs/browser/configure.zcml 2011-11-01 00:49:29 +0000
@@ -589,6 +589,12 @@
589 template="../templates/bug-edit.pt"589 template="../templates/bug-edit.pt"
590 permission="launchpad.Edit"/>590 permission="launchpad.Edit"/>
591 <browser:page591 <browser:page
592 name="+delete"
593 for="lp.bugs.interfaces.bugtask.IBugTask"
594 class="lp.bugs.browser.bugtask.BugTaskDeletionView"
595 template="../templates/bugtask-delete.pt"
596 permission="launchpad.Delete"/>
597 <browser:page
592 name="+secrecy"598 name="+secrecy"
593 for="lp.bugs.interfaces.bugtask.IBugTask"599 for="lp.bugs.interfaces.bugtask.IBugTask"
594 class="lp.bugs.browser.bug.BugSecrecyEditView"600 class="lp.bugs.browser.bug.BugSecrecyEditView"
595601
=== modified file 'lib/lp/bugs/browser/tests/test_bugtask.py'
--- lib/lp/bugs/browser/tests/test_bugtask.py 2011-10-31 15:28:28 +0000
+++ lib/lp/bugs/browser/tests/test_bugtask.py 2011-11-01 00:49:29 +0000
@@ -36,6 +36,11 @@
36 )36 )
37from canonical.launchpad.testing.pages import find_tag_by_id37from canonical.launchpad.testing.pages import find_tag_by_id
38from canonical.launchpad.webapp import canonical_url38from canonical.launchpad.webapp import canonical_url
39from canonical.launchpad.webapp.authorization import clear_cache
40from canonical.launchpad.webapp.interfaces import (
41 ILaunchBag,
42 ILaunchpadRoot,
43 )
39from canonical.launchpad.webapp.servers import LaunchpadTestRequest44from canonical.launchpad.webapp.servers import LaunchpadTestRequest
40from canonical.testing.layers import (45from canonical.testing.layers import (
41 DatabaseFunctionalLayer,46 DatabaseFunctionalLayer,
@@ -82,6 +87,9 @@
82from lp.testing.views import create_initialized_view87from lp.testing.views import create_initialized_view
8388
8489
90DELETE_BUGTASK_ENABLED = {u"disclosure.delete_bugtask.enabled": u"on"}
91
92
85class TestBugTaskView(TestCaseWithFactory):93class TestBugTaskView(TestCaseWithFactory):
8694
87 layer = LaunchpadFunctionalLayer95 layer = LaunchpadFunctionalLayer
@@ -617,6 +625,116 @@
617 self.assertIn(series.product.displayname, content)625 self.assertIn(series.product.displayname, content)
618626
619627
628class TestBugTaskDeleteLinks(TestCaseWithFactory):
629 """ Test that the delete icons/links are correctly rendered.
630
631 Bug task deletion is protected by a feature flag.
632 """
633
634 layer = DatabaseFunctionalLayer
635
636 def test_cannot_delete_only_bugtask(self):
637 # The last bugtask cannot be deleted.
638 bug = self.factory.makeBug()
639 login_person(bug.owner)
640 view = create_initialized_view(
641 bug, name='+bugtasks-and-nominations-table')
642 row_view = view._getTableRowView(bug.default_bugtask, False, False)
643 self.assertFalse(row_view.user_can_delete_bugtask)
644 del get_property_cache(row_view).user_can_delete_bugtask
645 with FeatureFixture(DELETE_BUGTASK_ENABLED):
646 self.assertFalse(row_view.user_can_delete_bugtask)
647
648 def test_can_delete_bugtask_if_authorised(self):
649 # The bugtask can be deleted if the user if authorised.
650 bug = self.factory.makeBug()
651 bugtask = self.factory.makeBugTask(bug=bug)
652 login_person(bugtask.owner)
653 view = create_initialized_view(
654 bug, name='+bugtasks-and-nominations-table',
655 principal=bugtask.owner)
656 row_view = view._getTableRowView(bugtask, False, False)
657 self.assertFalse(row_view.user_can_delete_bugtask)
658 del get_property_cache(row_view).user_can_delete_bugtask
659 clear_cache()
660 with FeatureFixture(DELETE_BUGTASK_ENABLED):
661 self.assertTrue(row_view.user_can_delete_bugtask)
662
663 def test_bugtask_delete_icon(self):
664 # The bugtask delete icon is rendered correctly for those tasks the
665 # user is allowed to delete.
666 bug = self.factory.makeBug()
667 bugtask_owner = self.factory.makePerson()
668 bugtask = self.factory.makeBugTask(bug=bug, owner=bugtask_owner)
669 with FeatureFixture(DELETE_BUGTASK_ENABLED):
670 login_person(bugtask.owner)
671 getUtility(ILaunchBag).add(bug.default_bugtask)
672 view = create_initialized_view(
673 bug, name='+bugtasks-and-nominations-table',
674 principal=bugtask.owner)
675 # We render the bug task table rows - there are 2 bug tasks.
676 subviews = view.getBugTaskAndNominationViews()
677 self.assertEqual(2, len(subviews))
678 default_bugtask_contents = subviews[0]()
679 bugtask_contents = subviews[1]()
680 # bugtask can be deleted because the user owns it.
681 delete_icon = find_tag_by_id(
682 bugtask_contents, 'bugtask-delete-task%d' % bugtask.id)
683 delete_url = canonical_url(
684 bugtask, rootsite='bugs', view_name='+delete')
685 self.assertEqual(delete_url, delete_icon['href'])
686 # default_bugtask cannot be deleted.
687 delete_icon = find_tag_by_id(
688 default_bugtask_contents,
689 'bugtask-delete-task%d' % bug.default_bugtask.id)
690 self.assertIsNone(delete_icon)
691
692
693class TestBugTaskDeleteView(TestCaseWithFactory):
694 """Test the bug task delete form."""
695
696 layer = DatabaseFunctionalLayer
697
698 def test_delete_view_rendering(self):
699 # Test the view rendering, including confirmation message, cancel url.
700 bug = self.factory.makeBug()
701 bugtask = self.factory.makeBugTask(bug=bug)
702 bug_url = canonical_url(bugtask.bug, rootsite='bugs')
703 # Set up request so that the ReturnToReferrerMixin can correctly
704 # extra the referer url.
705 server_url = canonical_url(
706 getUtility(ILaunchpadRoot), rootsite='bugs')
707 extra = {'HTTP_REFERER': bug_url}
708 with FeatureFixture(DELETE_BUGTASK_ENABLED):
709 login_person(bugtask.owner)
710 view = create_initialized_view(
711 bugtask, name='+delete', principal=bugtask.owner,
712 server_url=server_url, **extra)
713 contents = view.render()
714 confirmation_message = find_tag_by_id(
715 contents, 'confirmation-message')
716 self.assertIsNotNone(confirmation_message)
717 self.assertEqual(bug_url, view.cancel_url)
718
719 def test_delete_action(self):
720 # Test that the delete action works as expected.
721 bug = self.factory.makeBug()
722 bugtask = self.factory.makeBugTask(bug=bug)
723 target_name = bugtask.bugtargetdisplayname
724 with FeatureFixture(DELETE_BUGTASK_ENABLED):
725 login_person(bugtask.owner)
726 form = {
727 'field.actions.delete_bugtask': 'Delete',
728 }
729 view = create_initialized_view(
730 bugtask, name='+delete', form=form, principal=bugtask.owner)
731 self.assertEqual([bug.default_bugtask], bug.bugtasks)
732 notifications = view.request.response.notifications
733 self.assertEqual(1, len(notifications))
734 expected = 'This bug no longer affects %s.' % target_name
735 self.assertEqual(expected, notifications[0].message)
736
737
620class TestBugTasksAndNominationsViewAlsoAffects(TestCaseWithFactory):738class TestBugTasksAndNominationsViewAlsoAffects(TestCaseWithFactory):
621 """ Tests the boolean methods on the view used to indicate whether the739 """ Tests the boolean methods on the view used to indicate whether the
622 Also Affects... links should be allowed or not. Currently these740 Also Affects... links should be allowed or not. Currently these
623741
=== modified file 'lib/lp/bugs/configure.zcml'
--- lib/lp/bugs/configure.zcml 2011-10-25 02:12:44 +0000
+++ lib/lp/bugs/configure.zcml 2011-11-01 00:49:29 +0000
@@ -194,6 +194,7 @@
194 task_age194 task_age
195 bug_subscribers195 bug_subscribers
196 is_complete196 is_complete
197 canBeDeleted
197 canTransitionToStatus198 canTransitionToStatus
198 isSubscribed199 isSubscribed
199 getPackageComponent200 getPackageComponent
200201
=== added file 'lib/lp/bugs/templates/bugtask-delete.pt'
--- lib/lp/bugs/templates/bugtask-delete.pt 1970-01-01 00:00:00 +0000
+++ lib/lp/bugs/templates/bugtask-delete.pt 2011-11-01 00:49:29 +0000
@@ -0,0 +1,26 @@
1<html
2 xmlns="http://www.w3.org/1999/xhtml"
3 xmlns:tal="http://xml.zope.org/namespaces/tal"
4 xmlns:metal="http://xml.zope.org/namespaces/metal"
5 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
6 metal:use-macro="view/macro:page/main_only"
7 i18n:domain="launchpad">
8<body>
9
10<div metal:fill-slot="main">
11 <div metal:use-macro="context/@@launchpad_form/form">
12 <div id='confirmation-message' metal:fill-slot="extra_info">
13 <p class="large-warning" style="padding:2px 2px 0 36px;">
14 You are about to mark bug
15 "<tal:bug replace="context/bug/title">some bug</tal:bug>"
16 <br>as no longer affecting
17 <tal:target
18 replace="context/target/bugtargetdisplayname">some target
19 </tal:target>.
20 <br><br>
21 <strong>Please confirm you really want to do this.</strong></p>
22 </div>
23 </div>
24</div>
25</body>
26</html>
027
=== modified file 'lib/lp/bugs/templates/bugtask-tasks-and-nominations-table-row.pt'
--- lib/lp/bugs/templates/bugtask-tasks-and-nominations-table-row.pt 2011-08-19 08:55:04 +0000
+++ lib/lp/bugs/templates/bugtask-tasks-and-nominations-table-row.pt 2011-11-01 00:49:29 +0000
@@ -18,6 +18,11 @@
18 tal:content="view/getSeriesTargetName"18 tal:content="view/getSeriesTargetName"
19 />19 />
20 </tal:not-conjoined-task>20 </tal:not-conjoined-task>
21 <a tal:condition="data/user_can_delete"
22 tal:attributes="
23 id string:bugtask-delete-${data/form_row_id};
24 href data/delete_link"
25 class="sprite remove bugtask-delete" style="margin-left: 4px"></a>
21 </td>26 </td>
22 <td tal:condition="not:data/indent_task">27 <td tal:condition="not:data/indent_task">
23 <span tal:attributes="id string:bugtarget-picker-${data/row_id}">28 <span tal:attributes="id string:bugtarget-picker-${data/row_id}">
@@ -33,6 +38,11 @@
33 Edit38 Edit
34 </button>39 </button>
35 <div class="yui3-activator-message-box yui3-activator-hidden"></div>40 <div class="yui3-activator-message-box yui3-activator-hidden"></div>
41 <a tal:condition="data/user_can_delete"
42 tal:attributes="
43 id string:bugtask-delete-${data/form_row_id};
44 href data/delete_link"
45 class="sprite remove bugtask-delete" style="margin-left: 4px"></a>
36 </span>46 </span>
37 </td>47 </td>
3848
3949
=== modified file 'lib/lp/testing/views.py'
--- lib/lp/testing/views.py 2011-10-31 04:46:01 +0000
+++ lib/lp/testing/views.py 2011-11-01 00:49:29 +0000
@@ -82,7 +82,7 @@
82 server_url=None, method=None, principal=None,82 server_url=None, method=None, principal=None,
83 query_string=None, cookie=None, request=None,83 query_string=None, cookie=None, request=None,
84 path_info='/', rootsite=None,84 path_info='/', rootsite=None,
85 current_request=False):85 current_request=False, **kwargs):
86 """Return a view that has already been initialized."""86 """Return a view that has already been initialized."""
87 if method is None:87 if method is None:
88 if form is None:88 if form is None:
@@ -92,7 +92,7 @@
92 view = create_view(92 view = create_view(
93 context, name, form, layer, server_url, method, principal,93 context, name, form, layer, server_url, method, principal,
94 query_string, cookie, request, path_info, rootsite=rootsite,94 query_string, cookie, request, path_info, rootsite=rootsite,
95 current_request=current_request)95 current_request=current_request, **kwargs)
96 view.initialize()96 view.initialize()
97 return view97 return view
9898