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
1=== modified file 'lib/lp/app/javascript/activator/assets/skins/sam/activator-skin.css'
2--- lib/lp/app/javascript/activator/assets/skins/sam/activator-skin.css 2011-06-29 14:56:15 +0000
3+++ lib/lp/app/javascript/activator/assets/skins/sam/activator-skin.css 2011-11-01 00:49:29 +0000
4@@ -2,6 +2,7 @@
5
6 .yui3-skin-sam button.yui3-activator-act {
7 background: url('edit.png') 0 0 no-repeat;
8+ vertical-align: middle;
9 }
10
11 .yui3-skin-sam .yui3-activator-processing button.yui3-activator-act {
12
13=== modified file 'lib/lp/bugs/browser/bugtask.py'
14--- lib/lp/bugs/browser/bugtask.py 2011-10-31 15:28:28 +0000
15+++ lib/lp/bugs/browser/bugtask.py 2011-11-01 00:49:29 +0000
16@@ -18,6 +18,7 @@
17 'BugTaskBreadcrumb',
18 'BugTaskContextMenu',
19 'BugTaskCreateQuestionView',
20+ 'BugTaskDeletionView',
21 'BugTaskEditView',
22 'BugTaskExpirableListingView',
23 'BugTaskListingItem',
24@@ -159,6 +160,7 @@
25 custom_widget,
26 LaunchpadEditFormView,
27 LaunchpadFormView,
28+ ReturnToReferrerMixin,
29 )
30 from lp.app.browser.lazrjs import (
31 TextAreaEditorWidget,
32@@ -1760,6 +1762,24 @@
33 self.updateContextFromData(data)
34
35
36+class BugTaskDeletionView(ReturnToReferrerMixin, LaunchpadFormView):
37+ """Used to delete a bugtask."""
38+
39+ schema = IBugTask
40+ field_names = []
41+
42+ label = 'Remove bug task'
43+ page_title = label
44+
45+ @action('Delete', name='delete_bugtask')
46+ def delete_bugtask_action(self, action, data):
47+ bugtask = self.context
48+ message = ("This bug no longer affects %s."
49+ % bugtask.target.bugtargetdisplayname)
50+ bugtask.delete()
51+ self.request.response.addNotification(message)
52+
53+
54 class BugTaskListingView(LaunchpadView):
55 """A view designed for displaying bug tasks in lists."""
56 # Note that this right now is only used in tests and to render
57@@ -3646,7 +3666,9 @@
58 def initialize(self):
59 super(BugTaskTableRowView, self).initialize()
60 link = canonical_url(self.context)
61- task_link = edit_link = link + '/+editstatus'
62+ task_link = edit_link = canonical_url(
63+ self.context, view_name='+editstatus')
64+ delete_link = canonical_url(self.context, view_name='+delete')
65 can_edit = check_permission('launchpad.Edit', self.context)
66 bugtask_id = self.context.id
67 launchbag = getUtility(ILaunchBag)
68@@ -3668,6 +3690,8 @@
69 row_css_class='highlight' if is_primary else None,
70 target_link=canonical_url(self.context.target),
71 target_link_title=self.target_link_title,
72+ user_can_delete=self.user_can_delete_bugtask,
73+ delete_link=delete_link,
74 user_can_edit_importance=self.context.userCanEditImportance(
75 self.user),
76 importance_css_class='importance' + self.context.importance.name,
77@@ -3817,6 +3841,16 @@
78 """
79 return self.context.userCanEditMilestone(self.user)
80
81+ @cachedproperty
82+ def user_can_delete_bugtask(self):
83+ """Can the user delete the bug task?
84+
85+ If yes, return True, otherwise return False.
86+ """
87+ bugtask = self.context
88+ return (check_permission('launchpad.Delete', bugtask)
89+ and bugtask.canBeDeleted())
90+
91 @property
92 def style_for_add_milestone(self):
93 if self.context.milestone is None:
94
95=== modified file 'lib/lp/bugs/browser/configure.zcml'
96--- lib/lp/bugs/browser/configure.zcml 2011-10-03 07:49:31 +0000
97+++ lib/lp/bugs/browser/configure.zcml 2011-11-01 00:49:29 +0000
98@@ -589,6 +589,12 @@
99 template="../templates/bug-edit.pt"
100 permission="launchpad.Edit"/>
101 <browser:page
102+ name="+delete"
103+ for="lp.bugs.interfaces.bugtask.IBugTask"
104+ class="lp.bugs.browser.bugtask.BugTaskDeletionView"
105+ template="../templates/bugtask-delete.pt"
106+ permission="launchpad.Delete"/>
107+ <browser:page
108 name="+secrecy"
109 for="lp.bugs.interfaces.bugtask.IBugTask"
110 class="lp.bugs.browser.bug.BugSecrecyEditView"
111
112=== modified file 'lib/lp/bugs/browser/tests/test_bugtask.py'
113--- lib/lp/bugs/browser/tests/test_bugtask.py 2011-10-31 15:28:28 +0000
114+++ lib/lp/bugs/browser/tests/test_bugtask.py 2011-11-01 00:49:29 +0000
115@@ -36,6 +36,11 @@
116 )
117 from canonical.launchpad.testing.pages import find_tag_by_id
118 from canonical.launchpad.webapp import canonical_url
119+from canonical.launchpad.webapp.authorization import clear_cache
120+from canonical.launchpad.webapp.interfaces import (
121+ ILaunchBag,
122+ ILaunchpadRoot,
123+ )
124 from canonical.launchpad.webapp.servers import LaunchpadTestRequest
125 from canonical.testing.layers import (
126 DatabaseFunctionalLayer,
127@@ -82,6 +87,9 @@
128 from lp.testing.views import create_initialized_view
129
130
131+DELETE_BUGTASK_ENABLED = {u"disclosure.delete_bugtask.enabled": u"on"}
132+
133+
134 class TestBugTaskView(TestCaseWithFactory):
135
136 layer = LaunchpadFunctionalLayer
137@@ -617,6 +625,116 @@
138 self.assertIn(series.product.displayname, content)
139
140
141+class TestBugTaskDeleteLinks(TestCaseWithFactory):
142+ """ Test that the delete icons/links are correctly rendered.
143+
144+ Bug task deletion is protected by a feature flag.
145+ """
146+
147+ layer = DatabaseFunctionalLayer
148+
149+ def test_cannot_delete_only_bugtask(self):
150+ # The last bugtask cannot be deleted.
151+ bug = self.factory.makeBug()
152+ login_person(bug.owner)
153+ view = create_initialized_view(
154+ bug, name='+bugtasks-and-nominations-table')
155+ row_view = view._getTableRowView(bug.default_bugtask, False, False)
156+ self.assertFalse(row_view.user_can_delete_bugtask)
157+ del get_property_cache(row_view).user_can_delete_bugtask
158+ with FeatureFixture(DELETE_BUGTASK_ENABLED):
159+ self.assertFalse(row_view.user_can_delete_bugtask)
160+
161+ def test_can_delete_bugtask_if_authorised(self):
162+ # The bugtask can be deleted if the user if authorised.
163+ bug = self.factory.makeBug()
164+ bugtask = self.factory.makeBugTask(bug=bug)
165+ login_person(bugtask.owner)
166+ view = create_initialized_view(
167+ bug, name='+bugtasks-and-nominations-table',
168+ principal=bugtask.owner)
169+ row_view = view._getTableRowView(bugtask, False, False)
170+ self.assertFalse(row_view.user_can_delete_bugtask)
171+ del get_property_cache(row_view).user_can_delete_bugtask
172+ clear_cache()
173+ with FeatureFixture(DELETE_BUGTASK_ENABLED):
174+ self.assertTrue(row_view.user_can_delete_bugtask)
175+
176+ def test_bugtask_delete_icon(self):
177+ # The bugtask delete icon is rendered correctly for those tasks the
178+ # user is allowed to delete.
179+ bug = self.factory.makeBug()
180+ bugtask_owner = self.factory.makePerson()
181+ bugtask = self.factory.makeBugTask(bug=bug, owner=bugtask_owner)
182+ with FeatureFixture(DELETE_BUGTASK_ENABLED):
183+ login_person(bugtask.owner)
184+ getUtility(ILaunchBag).add(bug.default_bugtask)
185+ view = create_initialized_view(
186+ bug, name='+bugtasks-and-nominations-table',
187+ principal=bugtask.owner)
188+ # We render the bug task table rows - there are 2 bug tasks.
189+ subviews = view.getBugTaskAndNominationViews()
190+ self.assertEqual(2, len(subviews))
191+ default_bugtask_contents = subviews[0]()
192+ bugtask_contents = subviews[1]()
193+ # bugtask can be deleted because the user owns it.
194+ delete_icon = find_tag_by_id(
195+ bugtask_contents, 'bugtask-delete-task%d' % bugtask.id)
196+ delete_url = canonical_url(
197+ bugtask, rootsite='bugs', view_name='+delete')
198+ self.assertEqual(delete_url, delete_icon['href'])
199+ # default_bugtask cannot be deleted.
200+ delete_icon = find_tag_by_id(
201+ default_bugtask_contents,
202+ 'bugtask-delete-task%d' % bug.default_bugtask.id)
203+ self.assertIsNone(delete_icon)
204+
205+
206+class TestBugTaskDeleteView(TestCaseWithFactory):
207+ """Test the bug task delete form."""
208+
209+ layer = DatabaseFunctionalLayer
210+
211+ def test_delete_view_rendering(self):
212+ # Test the view rendering, including confirmation message, cancel url.
213+ bug = self.factory.makeBug()
214+ bugtask = self.factory.makeBugTask(bug=bug)
215+ bug_url = canonical_url(bugtask.bug, rootsite='bugs')
216+ # Set up request so that the ReturnToReferrerMixin can correctly
217+ # extra the referer url.
218+ server_url = canonical_url(
219+ getUtility(ILaunchpadRoot), rootsite='bugs')
220+ extra = {'HTTP_REFERER': bug_url}
221+ with FeatureFixture(DELETE_BUGTASK_ENABLED):
222+ login_person(bugtask.owner)
223+ view = create_initialized_view(
224+ bugtask, name='+delete', principal=bugtask.owner,
225+ server_url=server_url, **extra)
226+ contents = view.render()
227+ confirmation_message = find_tag_by_id(
228+ contents, 'confirmation-message')
229+ self.assertIsNotNone(confirmation_message)
230+ self.assertEqual(bug_url, view.cancel_url)
231+
232+ def test_delete_action(self):
233+ # Test that the delete action works as expected.
234+ bug = self.factory.makeBug()
235+ bugtask = self.factory.makeBugTask(bug=bug)
236+ target_name = bugtask.bugtargetdisplayname
237+ with FeatureFixture(DELETE_BUGTASK_ENABLED):
238+ login_person(bugtask.owner)
239+ form = {
240+ 'field.actions.delete_bugtask': 'Delete',
241+ }
242+ view = create_initialized_view(
243+ bugtask, name='+delete', form=form, principal=bugtask.owner)
244+ self.assertEqual([bug.default_bugtask], bug.bugtasks)
245+ notifications = view.request.response.notifications
246+ self.assertEqual(1, len(notifications))
247+ expected = 'This bug no longer affects %s.' % target_name
248+ self.assertEqual(expected, notifications[0].message)
249+
250+
251 class TestBugTasksAndNominationsViewAlsoAffects(TestCaseWithFactory):
252 """ Tests the boolean methods on the view used to indicate whether the
253 Also Affects... links should be allowed or not. Currently these
254
255=== modified file 'lib/lp/bugs/configure.zcml'
256--- lib/lp/bugs/configure.zcml 2011-10-25 02:12:44 +0000
257+++ lib/lp/bugs/configure.zcml 2011-11-01 00:49:29 +0000
258@@ -194,6 +194,7 @@
259 task_age
260 bug_subscribers
261 is_complete
262+ canBeDeleted
263 canTransitionToStatus
264 isSubscribed
265 getPackageComponent
266
267=== added file 'lib/lp/bugs/templates/bugtask-delete.pt'
268--- lib/lp/bugs/templates/bugtask-delete.pt 1970-01-01 00:00:00 +0000
269+++ lib/lp/bugs/templates/bugtask-delete.pt 2011-11-01 00:49:29 +0000
270@@ -0,0 +1,26 @@
271+<html
272+ xmlns="http://www.w3.org/1999/xhtml"
273+ xmlns:tal="http://xml.zope.org/namespaces/tal"
274+ xmlns:metal="http://xml.zope.org/namespaces/metal"
275+ xmlns:i18n="http://xml.zope.org/namespaces/i18n"
276+ metal:use-macro="view/macro:page/main_only"
277+ i18n:domain="launchpad">
278+<body>
279+
280+<div metal:fill-slot="main">
281+ <div metal:use-macro="context/@@launchpad_form/form">
282+ <div id='confirmation-message' metal:fill-slot="extra_info">
283+ <p class="large-warning" style="padding:2px 2px 0 36px;">
284+ You are about to mark bug
285+ "<tal:bug replace="context/bug/title">some bug</tal:bug>"
286+ <br>as no longer affecting
287+ <tal:target
288+ replace="context/target/bugtargetdisplayname">some target
289+ </tal:target>.
290+ <br><br>
291+ <strong>Please confirm you really want to do this.</strong></p>
292+ </div>
293+ </div>
294+</div>
295+</body>
296+</html>
297
298=== modified file 'lib/lp/bugs/templates/bugtask-tasks-and-nominations-table-row.pt'
299--- lib/lp/bugs/templates/bugtask-tasks-and-nominations-table-row.pt 2011-08-19 08:55:04 +0000
300+++ lib/lp/bugs/templates/bugtask-tasks-and-nominations-table-row.pt 2011-11-01 00:49:29 +0000
301@@ -18,6 +18,11 @@
302 tal:content="view/getSeriesTargetName"
303 />
304 </tal:not-conjoined-task>
305+ <a tal:condition="data/user_can_delete"
306+ tal:attributes="
307+ id string:bugtask-delete-${data/form_row_id};
308+ href data/delete_link"
309+ class="sprite remove bugtask-delete" style="margin-left: 4px"></a>
310 </td>
311 <td tal:condition="not:data/indent_task">
312 <span tal:attributes="id string:bugtarget-picker-${data/row_id}">
313@@ -33,6 +38,11 @@
314 Edit
315 </button>
316 <div class="yui3-activator-message-box yui3-activator-hidden"></div>
317+ <a tal:condition="data/user_can_delete"
318+ tal:attributes="
319+ id string:bugtask-delete-${data/form_row_id};
320+ href data/delete_link"
321+ class="sprite remove bugtask-delete" style="margin-left: 4px"></a>
322 </span>
323 </td>
324
325
326=== modified file 'lib/lp/testing/views.py'
327--- lib/lp/testing/views.py 2011-10-31 04:46:01 +0000
328+++ lib/lp/testing/views.py 2011-11-01 00:49:29 +0000
329@@ -82,7 +82,7 @@
330 server_url=None, method=None, principal=None,
331 query_string=None, cookie=None, request=None,
332 path_info='/', rootsite=None,
333- current_request=False):
334+ current_request=False, **kwargs):
335 """Return a view that has already been initialized."""
336 if method is None:
337 if form is None:
338@@ -92,7 +92,7 @@
339 view = create_view(
340 context, name, form, layer, server_url, method, principal,
341 query_string, cookie, request, path_info, rootsite=rootsite,
342- current_request=current_request)
343+ current_request=current_request, **kwargs)
344 view.initialize()
345 return view
346