Merge lp:~wallyworld/launchpad/delete-bugtask-ui-878909 into lp:launchpad
- delete-bugtask-ui-878909
- Merge into devel
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Curtis Hovey (community) | code | Approve | |
Review via email:
|
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://
== Tests ==
Add tests to browser/
TestBugTaskDele
(test that the delete links/icons are rendered as expected)
TestBugTaskDele
(test that the delete view itself works as expected)
== Lint ==
Checking for conflicts and issues in changed files.
Linting changed files:
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Ian Booth (wallyworld) wrote : | # |
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_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_
> will raise an exception if the named view is unregistered.
>
Yes, much better, thanks.
>>
>> +class TestBugTaskDele
>> + """ 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_
> bug, name='+
> principal=
I tried things like eg:
view = create_
rootsite=
view = create_
rootsite=
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-
> 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(
> return check_permi...
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
William Grant (wgrant) wrote : | # |
37 + @property
38 + def confirmation_
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.
44 + self.context.
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.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Ian Booth (wallyworld) wrote : | # |
On 26/10/11 08:36, William Grant wrote:
> 37 + @property
> 38 + def confirmation_
> 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.
> 44 + self.context.
>
> 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.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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_
>> target_
>> target_
>> + user_can_
>> + 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_
> 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_
> # The bugtask delete icon is rendered correctly for those tasks the
> # user is allowed to delete.
> bug = self.factory.
> bugtask = self.factory.
> with FeatureFixture(
> login_person(
> view = create_
> bug, name='+
> principal=
> content = view.render()
> # bugtask can be deleted because the user owns it.
> delete_icon = find_tag_by_id(
> content, 'bugtask-
> self.assertEqua
> # default_bugtask cannot be deleted.
> delete_icon = find_tag_by_id(
> content, 'bugtask-
> self.assertIsNo
>
I double checked my original work. I tried again using
> view = create_
> bug, name='+
> principal=
and it wouldn't render. I also tried other variations. So I think
TestBrowser is required for this test sadly.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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>
view = create_
# We render the bug task table rows - there are 2 bug tasks.
</new stuff>
# bugtask can be deleted because the user owns it.
# default_bugtask cannot be deleted.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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.
Preview Diff
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 | 2 | 2 | ||
6 | 3 | .yui3-skin-sam button.yui3-activator-act { | 3 | .yui3-skin-sam button.yui3-activator-act { |
7 | 4 | background: url('edit.png') 0 0 no-repeat; | 4 | background: url('edit.png') 0 0 no-repeat; |
8 | 5 | vertical-align: middle; | ||
9 | 5 | } | 6 | } |
10 | 6 | 7 | ||
11 | 7 | .yui3-skin-sam .yui3-activator-processing button.yui3-activator-act { | 8 | .yui3-skin-sam .yui3-activator-processing button.yui3-activator-act { |
12 | 8 | 9 | ||
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 | 18 | 'BugTaskBreadcrumb', | 18 | 'BugTaskBreadcrumb', |
18 | 19 | 'BugTaskContextMenu', | 19 | 'BugTaskContextMenu', |
19 | 20 | 'BugTaskCreateQuestionView', | 20 | 'BugTaskCreateQuestionView', |
20 | 21 | 'BugTaskDeletionView', | ||
21 | 21 | 'BugTaskEditView', | 22 | 'BugTaskEditView', |
22 | 22 | 'BugTaskExpirableListingView', | 23 | 'BugTaskExpirableListingView', |
23 | 23 | 'BugTaskListingItem', | 24 | 'BugTaskListingItem', |
24 | @@ -159,6 +160,7 @@ | |||
25 | 159 | custom_widget, | 160 | custom_widget, |
26 | 160 | LaunchpadEditFormView, | 161 | LaunchpadEditFormView, |
27 | 161 | LaunchpadFormView, | 162 | LaunchpadFormView, |
28 | 163 | ReturnToReferrerMixin, | ||
29 | 162 | ) | 164 | ) |
30 | 163 | from lp.app.browser.lazrjs import ( | 165 | from lp.app.browser.lazrjs import ( |
31 | 164 | TextAreaEditorWidget, | 166 | TextAreaEditorWidget, |
32 | @@ -1760,6 +1762,24 @@ | |||
33 | 1760 | self.updateContextFromData(data) | 1762 | self.updateContextFromData(data) |
34 | 1761 | 1763 | ||
35 | 1762 | 1764 | ||
36 | 1765 | class BugTaskDeletionView(ReturnToReferrerMixin, LaunchpadFormView): | ||
37 | 1766 | """Used to delete a bugtask.""" | ||
38 | 1767 | |||
39 | 1768 | schema = IBugTask | ||
40 | 1769 | field_names = [] | ||
41 | 1770 | |||
42 | 1771 | label = 'Remove bug task' | ||
43 | 1772 | page_title = label | ||
44 | 1773 | |||
45 | 1774 | @action('Delete', name='delete_bugtask') | ||
46 | 1775 | def delete_bugtask_action(self, action, data): | ||
47 | 1776 | bugtask = self.context | ||
48 | 1777 | message = ("This bug no longer affects %s." | ||
49 | 1778 | % bugtask.target.bugtargetdisplayname) | ||
50 | 1779 | bugtask.delete() | ||
51 | 1780 | self.request.response.addNotification(message) | ||
52 | 1781 | |||
53 | 1782 | |||
54 | 1763 | class BugTaskListingView(LaunchpadView): | 1783 | class BugTaskListingView(LaunchpadView): |
55 | 1764 | """A view designed for displaying bug tasks in lists.""" | 1784 | """A view designed for displaying bug tasks in lists.""" |
56 | 1765 | # Note that this right now is only used in tests and to render | 1785 | # Note that this right now is only used in tests and to render |
57 | @@ -3646,7 +3666,9 @@ | |||
58 | 3646 | def initialize(self): | 3666 | def initialize(self): |
59 | 3647 | super(BugTaskTableRowView, self).initialize() | 3667 | super(BugTaskTableRowView, self).initialize() |
60 | 3648 | link = canonical_url(self.context) | 3668 | link = canonical_url(self.context) |
62 | 3649 | task_link = edit_link = link + '/+editstatus' | 3669 | task_link = edit_link = canonical_url( |
63 | 3670 | self.context, view_name='+editstatus') | ||
64 | 3671 | delete_link = canonical_url(self.context, view_name='+delete') | ||
65 | 3650 | can_edit = check_permission('launchpad.Edit', self.context) | 3672 | can_edit = check_permission('launchpad.Edit', self.context) |
66 | 3651 | bugtask_id = self.context.id | 3673 | bugtask_id = self.context.id |
67 | 3652 | launchbag = getUtility(ILaunchBag) | 3674 | launchbag = getUtility(ILaunchBag) |
68 | @@ -3668,6 +3690,8 @@ | |||
69 | 3668 | row_css_class='highlight' if is_primary else None, | 3690 | row_css_class='highlight' if is_primary else None, |
70 | 3669 | target_link=canonical_url(self.context.target), | 3691 | target_link=canonical_url(self.context.target), |
71 | 3670 | target_link_title=self.target_link_title, | 3692 | target_link_title=self.target_link_title, |
72 | 3693 | user_can_delete=self.user_can_delete_bugtask, | ||
73 | 3694 | delete_link=delete_link, | ||
74 | 3671 | user_can_edit_importance=self.context.userCanEditImportance( | 3695 | user_can_edit_importance=self.context.userCanEditImportance( |
75 | 3672 | self.user), | 3696 | self.user), |
76 | 3673 | importance_css_class='importance' + self.context.importance.name, | 3697 | importance_css_class='importance' + self.context.importance.name, |
77 | @@ -3817,6 +3841,16 @@ | |||
78 | 3817 | """ | 3841 | """ |
79 | 3818 | return self.context.userCanEditMilestone(self.user) | 3842 | return self.context.userCanEditMilestone(self.user) |
80 | 3819 | 3843 | ||
81 | 3844 | @cachedproperty | ||
82 | 3845 | def user_can_delete_bugtask(self): | ||
83 | 3846 | """Can the user delete the bug task? | ||
84 | 3847 | |||
85 | 3848 | If yes, return True, otherwise return False. | ||
86 | 3849 | """ | ||
87 | 3850 | bugtask = self.context | ||
88 | 3851 | return (check_permission('launchpad.Delete', bugtask) | ||
89 | 3852 | and bugtask.canBeDeleted()) | ||
90 | 3853 | |||
91 | 3820 | @property | 3854 | @property |
92 | 3821 | def style_for_add_milestone(self): | 3855 | def style_for_add_milestone(self): |
93 | 3822 | if self.context.milestone is None: | 3856 | if self.context.milestone is None: |
94 | 3823 | 3857 | ||
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 | 589 | template="../templates/bug-edit.pt" | 589 | template="../templates/bug-edit.pt" |
100 | 590 | permission="launchpad.Edit"/> | 590 | permission="launchpad.Edit"/> |
101 | 591 | <browser:page | 591 | <browser:page |
102 | 592 | name="+delete" | ||
103 | 593 | for="lp.bugs.interfaces.bugtask.IBugTask" | ||
104 | 594 | class="lp.bugs.browser.bugtask.BugTaskDeletionView" | ||
105 | 595 | template="../templates/bugtask-delete.pt" | ||
106 | 596 | permission="launchpad.Delete"/> | ||
107 | 597 | <browser:page | ||
108 | 592 | name="+secrecy" | 598 | name="+secrecy" |
109 | 593 | for="lp.bugs.interfaces.bugtask.IBugTask" | 599 | for="lp.bugs.interfaces.bugtask.IBugTask" |
110 | 594 | class="lp.bugs.browser.bug.BugSecrecyEditView" | 600 | class="lp.bugs.browser.bug.BugSecrecyEditView" |
111 | 595 | 601 | ||
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 | 36 | ) | 36 | ) |
117 | 37 | from canonical.launchpad.testing.pages import find_tag_by_id | 37 | from canonical.launchpad.testing.pages import find_tag_by_id |
118 | 38 | from canonical.launchpad.webapp import canonical_url | 38 | from canonical.launchpad.webapp import canonical_url |
119 | 39 | from canonical.launchpad.webapp.authorization import clear_cache | ||
120 | 40 | from canonical.launchpad.webapp.interfaces import ( | ||
121 | 41 | ILaunchBag, | ||
122 | 42 | ILaunchpadRoot, | ||
123 | 43 | ) | ||
124 | 39 | from canonical.launchpad.webapp.servers import LaunchpadTestRequest | 44 | from canonical.launchpad.webapp.servers import LaunchpadTestRequest |
125 | 40 | from canonical.testing.layers import ( | 45 | from canonical.testing.layers import ( |
126 | 41 | DatabaseFunctionalLayer, | 46 | DatabaseFunctionalLayer, |
127 | @@ -82,6 +87,9 @@ | |||
128 | 82 | from lp.testing.views import create_initialized_view | 87 | from lp.testing.views import create_initialized_view |
129 | 83 | 88 | ||
130 | 84 | 89 | ||
131 | 90 | DELETE_BUGTASK_ENABLED = {u"disclosure.delete_bugtask.enabled": u"on"} | ||
132 | 91 | |||
133 | 92 | |||
134 | 85 | class TestBugTaskView(TestCaseWithFactory): | 93 | class TestBugTaskView(TestCaseWithFactory): |
135 | 86 | 94 | ||
136 | 87 | layer = LaunchpadFunctionalLayer | 95 | layer = LaunchpadFunctionalLayer |
137 | @@ -617,6 +625,116 @@ | |||
138 | 617 | self.assertIn(series.product.displayname, content) | 625 | self.assertIn(series.product.displayname, content) |
139 | 618 | 626 | ||
140 | 619 | 627 | ||
141 | 628 | class TestBugTaskDeleteLinks(TestCaseWithFactory): | ||
142 | 629 | """ Test that the delete icons/links are correctly rendered. | ||
143 | 630 | |||
144 | 631 | Bug task deletion is protected by a feature flag. | ||
145 | 632 | """ | ||
146 | 633 | |||
147 | 634 | layer = DatabaseFunctionalLayer | ||
148 | 635 | |||
149 | 636 | def test_cannot_delete_only_bugtask(self): | ||
150 | 637 | # The last bugtask cannot be deleted. | ||
151 | 638 | bug = self.factory.makeBug() | ||
152 | 639 | login_person(bug.owner) | ||
153 | 640 | view = create_initialized_view( | ||
154 | 641 | bug, name='+bugtasks-and-nominations-table') | ||
155 | 642 | row_view = view._getTableRowView(bug.default_bugtask, False, False) | ||
156 | 643 | self.assertFalse(row_view.user_can_delete_bugtask) | ||
157 | 644 | del get_property_cache(row_view).user_can_delete_bugtask | ||
158 | 645 | with FeatureFixture(DELETE_BUGTASK_ENABLED): | ||
159 | 646 | self.assertFalse(row_view.user_can_delete_bugtask) | ||
160 | 647 | |||
161 | 648 | def test_can_delete_bugtask_if_authorised(self): | ||
162 | 649 | # The bugtask can be deleted if the user if authorised. | ||
163 | 650 | bug = self.factory.makeBug() | ||
164 | 651 | bugtask = self.factory.makeBugTask(bug=bug) | ||
165 | 652 | login_person(bugtask.owner) | ||
166 | 653 | view = create_initialized_view( | ||
167 | 654 | bug, name='+bugtasks-and-nominations-table', | ||
168 | 655 | principal=bugtask.owner) | ||
169 | 656 | row_view = view._getTableRowView(bugtask, False, False) | ||
170 | 657 | self.assertFalse(row_view.user_can_delete_bugtask) | ||
171 | 658 | del get_property_cache(row_view).user_can_delete_bugtask | ||
172 | 659 | clear_cache() | ||
173 | 660 | with FeatureFixture(DELETE_BUGTASK_ENABLED): | ||
174 | 661 | self.assertTrue(row_view.user_can_delete_bugtask) | ||
175 | 662 | |||
176 | 663 | def test_bugtask_delete_icon(self): | ||
177 | 664 | # The bugtask delete icon is rendered correctly for those tasks the | ||
178 | 665 | # user is allowed to delete. | ||
179 | 666 | bug = self.factory.makeBug() | ||
180 | 667 | bugtask_owner = self.factory.makePerson() | ||
181 | 668 | bugtask = self.factory.makeBugTask(bug=bug, owner=bugtask_owner) | ||
182 | 669 | with FeatureFixture(DELETE_BUGTASK_ENABLED): | ||
183 | 670 | login_person(bugtask.owner) | ||
184 | 671 | getUtility(ILaunchBag).add(bug.default_bugtask) | ||
185 | 672 | view = create_initialized_view( | ||
186 | 673 | bug, name='+bugtasks-and-nominations-table', | ||
187 | 674 | principal=bugtask.owner) | ||
188 | 675 | # We render the bug task table rows - there are 2 bug tasks. | ||
189 | 676 | subviews = view.getBugTaskAndNominationViews() | ||
190 | 677 | self.assertEqual(2, len(subviews)) | ||
191 | 678 | default_bugtask_contents = subviews[0]() | ||
192 | 679 | bugtask_contents = subviews[1]() | ||
193 | 680 | # bugtask can be deleted because the user owns it. | ||
194 | 681 | delete_icon = find_tag_by_id( | ||
195 | 682 | bugtask_contents, 'bugtask-delete-task%d' % bugtask.id) | ||
196 | 683 | delete_url = canonical_url( | ||
197 | 684 | bugtask, rootsite='bugs', view_name='+delete') | ||
198 | 685 | self.assertEqual(delete_url, delete_icon['href']) | ||
199 | 686 | # default_bugtask cannot be deleted. | ||
200 | 687 | delete_icon = find_tag_by_id( | ||
201 | 688 | default_bugtask_contents, | ||
202 | 689 | 'bugtask-delete-task%d' % bug.default_bugtask.id) | ||
203 | 690 | self.assertIsNone(delete_icon) | ||
204 | 691 | |||
205 | 692 | |||
206 | 693 | class TestBugTaskDeleteView(TestCaseWithFactory): | ||
207 | 694 | """Test the bug task delete form.""" | ||
208 | 695 | |||
209 | 696 | layer = DatabaseFunctionalLayer | ||
210 | 697 | |||
211 | 698 | def test_delete_view_rendering(self): | ||
212 | 699 | # Test the view rendering, including confirmation message, cancel url. | ||
213 | 700 | bug = self.factory.makeBug() | ||
214 | 701 | bugtask = self.factory.makeBugTask(bug=bug) | ||
215 | 702 | bug_url = canonical_url(bugtask.bug, rootsite='bugs') | ||
216 | 703 | # Set up request so that the ReturnToReferrerMixin can correctly | ||
217 | 704 | # extra the referer url. | ||
218 | 705 | server_url = canonical_url( | ||
219 | 706 | getUtility(ILaunchpadRoot), rootsite='bugs') | ||
220 | 707 | extra = {'HTTP_REFERER': bug_url} | ||
221 | 708 | with FeatureFixture(DELETE_BUGTASK_ENABLED): | ||
222 | 709 | login_person(bugtask.owner) | ||
223 | 710 | view = create_initialized_view( | ||
224 | 711 | bugtask, name='+delete', principal=bugtask.owner, | ||
225 | 712 | server_url=server_url, **extra) | ||
226 | 713 | contents = view.render() | ||
227 | 714 | confirmation_message = find_tag_by_id( | ||
228 | 715 | contents, 'confirmation-message') | ||
229 | 716 | self.assertIsNotNone(confirmation_message) | ||
230 | 717 | self.assertEqual(bug_url, view.cancel_url) | ||
231 | 718 | |||
232 | 719 | def test_delete_action(self): | ||
233 | 720 | # Test that the delete action works as expected. | ||
234 | 721 | bug = self.factory.makeBug() | ||
235 | 722 | bugtask = self.factory.makeBugTask(bug=bug) | ||
236 | 723 | target_name = bugtask.bugtargetdisplayname | ||
237 | 724 | with FeatureFixture(DELETE_BUGTASK_ENABLED): | ||
238 | 725 | login_person(bugtask.owner) | ||
239 | 726 | form = { | ||
240 | 727 | 'field.actions.delete_bugtask': 'Delete', | ||
241 | 728 | } | ||
242 | 729 | view = create_initialized_view( | ||
243 | 730 | bugtask, name='+delete', form=form, principal=bugtask.owner) | ||
244 | 731 | self.assertEqual([bug.default_bugtask], bug.bugtasks) | ||
245 | 732 | notifications = view.request.response.notifications | ||
246 | 733 | self.assertEqual(1, len(notifications)) | ||
247 | 734 | expected = 'This bug no longer affects %s.' % target_name | ||
248 | 735 | self.assertEqual(expected, notifications[0].message) | ||
249 | 736 | |||
250 | 737 | |||
251 | 620 | class TestBugTasksAndNominationsViewAlsoAffects(TestCaseWithFactory): | 738 | class TestBugTasksAndNominationsViewAlsoAffects(TestCaseWithFactory): |
252 | 621 | """ Tests the boolean methods on the view used to indicate whether the | 739 | """ Tests the boolean methods on the view used to indicate whether the |
253 | 622 | Also Affects... links should be allowed or not. Currently these | 740 | Also Affects... links should be allowed or not. Currently these |
254 | 623 | 741 | ||
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 | 194 | task_age | 194 | task_age |
260 | 195 | bug_subscribers | 195 | bug_subscribers |
261 | 196 | is_complete | 196 | is_complete |
262 | 197 | canBeDeleted | ||
263 | 197 | canTransitionToStatus | 198 | canTransitionToStatus |
264 | 198 | isSubscribed | 199 | isSubscribed |
265 | 199 | getPackageComponent | 200 | getPackageComponent |
266 | 200 | 201 | ||
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 | 1 | <html | ||
272 | 2 | xmlns="http://www.w3.org/1999/xhtml" | ||
273 | 3 | xmlns:tal="http://xml.zope.org/namespaces/tal" | ||
274 | 4 | xmlns:metal="http://xml.zope.org/namespaces/metal" | ||
275 | 5 | xmlns:i18n="http://xml.zope.org/namespaces/i18n" | ||
276 | 6 | metal:use-macro="view/macro:page/main_only" | ||
277 | 7 | i18n:domain="launchpad"> | ||
278 | 8 | <body> | ||
279 | 9 | |||
280 | 10 | <div metal:fill-slot="main"> | ||
281 | 11 | <div metal:use-macro="context/@@launchpad_form/form"> | ||
282 | 12 | <div id='confirmation-message' metal:fill-slot="extra_info"> | ||
283 | 13 | <p class="large-warning" style="padding:2px 2px 0 36px;"> | ||
284 | 14 | You are about to mark bug | ||
285 | 15 | "<tal:bug replace="context/bug/title">some bug</tal:bug>" | ||
286 | 16 | <br>as no longer affecting | ||
287 | 17 | <tal:target | ||
288 | 18 | replace="context/target/bugtargetdisplayname">some target | ||
289 | 19 | </tal:target>. | ||
290 | 20 | <br><br> | ||
291 | 21 | <strong>Please confirm you really want to do this.</strong></p> | ||
292 | 22 | </div> | ||
293 | 23 | </div> | ||
294 | 24 | </div> | ||
295 | 25 | </body> | ||
296 | 26 | </html> | ||
297 | 0 | 27 | ||
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 | 18 | tal:content="view/getSeriesTargetName" | 18 | tal:content="view/getSeriesTargetName" |
303 | 19 | /> | 19 | /> |
304 | 20 | </tal:not-conjoined-task> | 20 | </tal:not-conjoined-task> |
305 | 21 | <a tal:condition="data/user_can_delete" | ||
306 | 22 | tal:attributes=" | ||
307 | 23 | id string:bugtask-delete-${data/form_row_id}; | ||
308 | 24 | href data/delete_link" | ||
309 | 25 | class="sprite remove bugtask-delete" style="margin-left: 4px"></a> | ||
310 | 21 | </td> | 26 | </td> |
311 | 22 | <td tal:condition="not:data/indent_task"> | 27 | <td tal:condition="not:data/indent_task"> |
312 | 23 | <span tal:attributes="id string:bugtarget-picker-${data/row_id}"> | 28 | <span tal:attributes="id string:bugtarget-picker-${data/row_id}"> |
313 | @@ -33,6 +38,11 @@ | |||
314 | 33 | Edit | 38 | Edit |
315 | 34 | </button> | 39 | </button> |
316 | 35 | <div class="yui3-activator-message-box yui3-activator-hidden"></div> | 40 | <div class="yui3-activator-message-box yui3-activator-hidden"></div> |
317 | 41 | <a tal:condition="data/user_can_delete" | ||
318 | 42 | tal:attributes=" | ||
319 | 43 | id string:bugtask-delete-${data/form_row_id}; | ||
320 | 44 | href data/delete_link" | ||
321 | 45 | class="sprite remove bugtask-delete" style="margin-left: 4px"></a> | ||
322 | 36 | </span> | 46 | </span> |
323 | 37 | </td> | 47 | </td> |
324 | 38 | 48 | ||
325 | 39 | 49 | ||
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 | 82 | server_url=None, method=None, principal=None, | 82 | server_url=None, method=None, principal=None, |
331 | 83 | query_string=None, cookie=None, request=None, | 83 | query_string=None, cookie=None, request=None, |
332 | 84 | path_info='/', rootsite=None, | 84 | path_info='/', rootsite=None, |
334 | 85 | current_request=False): | 85 | current_request=False, **kwargs): |
335 | 86 | """Return a view that has already been initialized.""" | 86 | """Return a view that has already been initialized.""" |
336 | 87 | if method is None: | 87 | if method is None: |
337 | 88 | if form is None: | 88 | if form is None: |
338 | @@ -92,7 +92,7 @@ | |||
339 | 92 | view = create_view( | 92 | view = create_view( |
340 | 93 | context, name, form, layer, server_url, method, principal, | 93 | context, name, form, layer, server_url, method, principal, |
341 | 94 | query_string, cookie, request, path_info, rootsite=rootsite, | 94 | query_string, cookie, request, path_info, rootsite=rootsite, |
343 | 95 | current_request=current_request) | 95 | current_request=current_request, **kwargs) |
344 | 96 | view.initialize() | 96 | view.initialize() |
345 | 97 | return view | 97 | return view |
346 | 98 | 98 |
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' bugs/browser/ bugtask. py 2011-10-19 14:54:07 +0000 bugs/browser/ bugtask. py 2011-10-25 08:17:26 +0000 extFromData( data) View(LaunchpadF ormView) : message( self): bug.title, target. bugtargetdispla yname))
> --- lib/lp/
> +++ lib/lp/
> @@ -1757,6 +1758,38 @@
> self.updateCont
>
>
> +class BugTaskDeletion
> + """Used to delete a bugtask."""
> +
> + schema = IBugTask
> + field_names = []
> +
> + label = 'Remove bug task'
> + page_title = label
> +
> + @property
> + def confirmation_
> + 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.
> + self.context.
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 @@ class=' highlight' if is_primary else None, link=canonical_ url(self. context. target) , link_title= self.target_ link_title, delete= self.user_ can_delete_ bugtask,
> row_css_
> target_
> target_
> + user_can_
> + delete_link=link + '/+delete',
While I am certain this link will work, but it is a crafted link we need to url(self. context. target, view_name= '+delete' )
maintain with extra testing.
canonical_
will raise an exception if the named view is unregistered.
> === modified file 'lib/lp/ bugs/browser/ tests/test_ bugtask. py' bugs/browser/ tests/test_ bugtask. py 2011-10-05 18:02:45 +0000 bugs/browser/ tests/test_ bugtask. py 2011-10-25 08:17:26 +0000 series. product. displayname, content) teLinks( TestCaseWithFac tory):
> --- lib/lp/
> +++ lib/lp/
> @@ -606,6 +614,99 @@
> self.assertIn(
>
>
> +class TestBugTaskDele
> + """ 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 = DatabaseFunctio nalLayer delete_ only_bugtask( self): makeBug( ) bug.owner) initialized_ view( bugtasks- and-nominations -table' ) owView( bug.default_ bugtask, False, False) e(row_view. user_can_ delete_ bugtask) cache(row_ view).user_ can_delete_ bugtask DELETE_ BUGTASK_ ENABLED) : e(row_view. user_can_ delete_ bugtask) delete_ bugtask_ if_authorised( self): makeBug( )
+
+ def test_cannot_
+ # The last bugtask cannot be deleted.
+ bug = self.factory.
+ login_person(
+ view = create_
+ bug, name='+
+ row_view = view._getTableR
+ self.assertFals
+ del get_property_
+ with FeatureFixture(
+ self.assertFals
+
+ def test_can_
+ # The bugtask can be deleted if the user if authorised.
+ bug = self.factory.
+ bugtask = self.fac...