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

Proposed by Ian Booth
Status: Merged
Approved by: Ian Booth
Approved revision: no longer in the source branch.
Merged at revision: 14255
Proposed branch: lp:~wallyworld/launchpad/delete-bugtask-ui-ajax-878909
Merge into: lp:launchpad
Prerequisite: lp:~wallyworld/launchpad/delete-bugtask-ui-878909
Diff against target: 1400 lines (+850/-106)
17 files modified
lib/lp/app/widgets/templates/form-picker-macros.pt (+7/-2)
lib/lp/bugs/browser/bugtask.py (+85/-14)
lib/lp/bugs/browser/configure.zcml (+6/-0)
lib/lp/bugs/browser/tests/bug-views.txt (+14/-3)
lib/lp/bugs/browser/tests/test_bug_views.py (+1/-1)
lib/lp/bugs/browser/tests/test_bugtask.py (+117/-5)
lib/lp/bugs/javascript/bugtask_index.js (+243/-9)
lib/lp/bugs/javascript/subscribers.js (+6/-1)
lib/lp/bugs/javascript/tests/test_bugtask_delete.html (+81/-0)
lib/lp/bugs/javascript/tests/test_bugtask_delete.js (+223/-0)
lib/lp/bugs/javascript/tests/test_subscribers.js (+9/-2)
lib/lp/bugs/stories/bugs/xx-bug-index.txt (+7/-7)
lib/lp/bugs/stories/bugtask-management/xx-bugtask-edit-forms.txt (+6/-2)
lib/lp/bugs/templates/bugtask-index.pt (+2/-5)
lib/lp/bugs/templates/bugtask-tasks-and-nominations-table-row.pt (+15/-33)
lib/lp/bugs/templates/bugtasks-and-nominations-portal.pt (+1/-22)
lib/lp/bugs/templates/bugtasks-and-nominations-table.pt (+27/-0)
To merge this branch: bzr merge lp:~wallyworld/launchpad/delete-bugtask-ui-ajax-878909
Reviewer Review Type Date Requested Status
Raphaël Badin (community) Approve
Review via email: mp+80779@code.launchpad.net

Commit message

[r=rvb][bug=878909] Add ajax support for deleting bug tasks.

Description of the change

== Implementation ==

Requires feature flag: disclosure.delete_bugtask.enabled

1. The core ajax implementation is relatively straightforward:
- capture the click on the delete link
- perform a POST with url = <bug task url>/+delete
- the standard zope BugTaskDeleteView form performs the delete as for a HTML form submit
- the form detects an XHR request being used and renders and returns the HTML for the new bug tasks table
- the javascript caller replaces the old bugtasks table with the new HTML received from the XHR call

Some small refactoring was required to extract the tales required to render just the bug tasks table. The existing view called +bugtasks-and-nominations-table was renamed to +bugtasks-and-nominations-portal since it renders Affects Me Too and Also Affacts links as well as the table. The +bugtasks-and-nominations-table view renders just the bugtasks table. The tales used for the portal references the newly created table view, allowing the tales to be reused.

So first issue: the new bugtasks table is duly rendered but none of the javascript widgets are wired up. This is because some of the javascript is included as part of the tales used to render the picker widgets, while other javascript is only executed once on page load.

2. Extract bugtask row javascript from tales

The lp.bug.bugtak_index module already contains a method (setup_bugtask_row) for setting up a bug task row (wiring up importance/status widgets, adding expander etc). So the javascript from the bugtask-tasks-nominations-and-table-row.pt tales was moved into this existing method. The BugTaskTableRowView view had a js_config method which provided data for the javascript embedded in the tales. This was renamed to bugtask_config and was re-purposed to stuff data into the client cache, allowing the moved javascript to get at the data it needs.

An onload handler was added to execute a new setup_bugtask_table() method. This uses the client cache data and the newly refactored javascript to wire up the bug tasks table.

When the new bug tasks table is rendered after the XHR delete call, the same setup_bugtask_table() is called to do the wiring.

3. Bug found

There were conflicting implementations of userCanEditImportance() found. One was essentially:

bugtask.userCanEditImportance(self.user) and not bugtask.bugwatch

while the other left out the second bugwatch condition. This caused the edit icon to be incorrectly rendered for remote bug tasks. I've fixed it.

4. Wire the pickers

There's pickers for assignee and also project/source package selection in the expandable form for each bug task. The javascript to wire these up lives in the form-picker-macro.pt tales. So it's not feasible to rip this out and reuse as per item #2 above since this picker stuff is generic infrastructure and things would break. So I chose the following solution. The javascript function to do the picker wiring is registered in a new yui namespace "lp.app.picker.connect". The function is still run as per the current picker infrastructure but the function is also available to run later as and when required. So when the html for the new table is rendered, the picker widgets embedded in the table are re-wired using the previously registered functions.

5. Confirmation dialog

A confirmation dialog was added to guard against accidental deletion.

6. Problem - deleting the highlighted bug task

The page used to display list of bug tasks for a given bug is rendered as the bug index view as well as the index view for each individual bug task url. The bug task corresponding to the current url is highlighted and:
i) the client cache self_link and web_link values are for the highlighted bugtask
ii) the XHR links for duplicate marking, privacy setting etc are all relative to this url

When the current bug task is deleted, the table is correctly re-rendered and the bug's new default bug tasks is now the highlighted one. But the urls referred to above in the cache and links are now invalid.

One existing case was fixed - the subscribers portlet setup was changed to use the bug's url rather than the bug task.
The other cases need fixing though.

Options:
i) update the URLs using javascript code when the bug task table is re-rendered
ii) server side zope publisher redirect when invoked with the old bug task urls
iii) when highlighted bug task is deleted, in that case simply redirect to the bugtask url of the new default task. this causes a new page load but the urls will be correct

Option (iii) is the one that can be best implemented. It has its own problem though. The the server does a redirect during an XHR call, the redirect is followed and the rendered contents are returned to the Javascript caller with status 200. The caller interprets this data as if the original call completed without the redirect. So the new bugtask page is rendered over the top of the bugtask table. Not what we want.

To solve the problem, the view returns a JSON dict containing the new bugtask URL when a redirection is required. The caller then does the redirect itself. The existing ReturnToReferrerMixin class was used to determine the referring URL and hence whether the bugtask being deleted is the current one and hence whether redirection is required. There is a slight pause when the redirect happens but I'm not sure I can do much about that.

== Demo and QA ==

http://people.canonical.com/~ianb/delete_bugtask.ogv

== Tests ==

1. Renamed +bugtasks-and-nominations-table view

Add extra test to bug-views.txt and update existing tests to use the new view name.

2. New setup for bugs.javascript.subscribers.js

Update test_subscribers.js

3. New DeleteBugTaskView behaviour

Add new tests to TestDeleteBugTaskView to check the ajax behaviour:
- test_ajax_delete_non_current_bugtask
- test_ajax_delete_current_bugtask

4. New Javascript bugtask delete functionality

Add new yui test: bugs.javascript.tests.test_bugtask_delete.js

5. Under the covers picker changes etc

Reply on yuixhr tests ported from windmill tests (when done) since the windmill tests covered the rendering and wiring up of the pickers.

== Lint ==

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/app/widgets/templates/form-picker-macros.pt
  lib/lp/bugs/browser/bugtask.py
  lib/lp/bugs/browser/configure.zcml
  lib/lp/bugs/browser/tests/bug-views.txt
  lib/lp/bugs/browser/tests/test_bugtask.py
  lib/lp/bugs/javascript/bugtask_index.js
  lib/lp/bugs/javascript/subscribers.js
  lib/lp/bugs/javascript/tests/test_bugtask_delete.html
  lib/lp/bugs/javascript/tests/test_bugtask_delete.js
  lib/lp/bugs/javascript/tests/test_subscribers.js
  lib/lp/bugs/templates/bugtask-index.pt
  lib/lp/bugs/templates/bugtask-tasks-and-nominations-table-row.pt
  lib/lp/bugs/templates/bugtasks-and-nominations-portal.pt
  lib/lp/bugs/templates/bugtasks-and-nominations-table.pt

./lib/lp/bugs/javascript/bugtask_index.js
     640: Move 'var' declarations to the top of the function.
     640: Stopping. (46% scanned).
      -1: JSLINT had a fatal error.

To post a comment you must log in.
Revision history for this message
Raphaël Badin (rvb) wrote :
Download full text (7.5 KiB)

Hi Ian,

Excellent work. Thanks for the detailed MP and all the comments you put in the code, I really appreciate it.

A few remarks/suggestions. I suppose you're quite eager to land this so please take the nitpicks (especially the ones about indentation) with a grain of salt ;).

[0]

1249 lines. Please have mercy on us poor reviewers ;). I know Javascript is verbose and you've done a lot of refactoring but still, I'm sure you would have had a volunteer reviewer for this branch sooner if you had split it into 2 or 3. Also, the problem with enormous branches is that the odds of getting a conflict goes way up.

> 3. Bug found
> There were conflicting implementations of userCanEditImportance() found. One was essentially:
> bugtask.userCanEditImportance(self.user) and not bugtask.bugwatch

For instance you could have fixed this in another pipe (I don't know if you already use the bzr pipeline plugin but in case you don't, please check it out: http://wiki.bazaar.canonical.com/BzrPipeline).

[1]

294 + """ Test that the client cache contains the expected data.
295 +
296 + The cache data is used by the Javascript to enable the delete
297 + links to work as expected.
298 + """

Weird indentation for the second """.

[2]

312 + def check_bugtask_data(bugtask, can_delete):
313 + self.assertIn(bugtask.id, all_bugtask_data)

Don't you think it would be easier to read if you moved this method outside of the test method?

[3]

361 + self.assertEqual(
362 + view.request.response.getHeader('content-type'),
363 + 'application/json')

'application/json' should be the first argument, otherwise a failure will be difficult to interpret.

[4]

334 + bug = self.factory.makeBug()
335 + bugtask = self.factory.makeBugTask(bug=bug)
336 + target_name = bugtask.bugtargetdisplayname
337 + bugtask_url = canonical_url(bugtask, rootsite='bugs')

And

370 + bug = self.factory.makeBug()
371 + bugtask = self.factory.makeBugTask(bug=bug)
372 + target_name = bugtask.bugtargetdisplayname
373 + default_bugtask_url = canonical_url(
374 + bug.default_bugtask, rootsite='bugs')

Would you mind refactoring that code into a common setup method?

[5]

517 + if( bugtask_data.hasOwnProperty(id) ) {

small typos: if( bugtask_data.hasOwnProperty(id) ) { → if (bugtask_data.hasOwnProperty(id)) {

Same here:

532 + if( link.hasClass('js-action') ) {
537 + if( Y.Lang.isFunction(connect_func)) {
629 + if( content_type === 'application/json' ) {

[6]

591 + var delete_text = [
592 + '<p class="large-warning" style="padding:2px 2px 0 36px;">',
593 + 'You are about to mark bug "',
594 + conf.bug_title,
595 + '"<br>as no longer affecting ',
596 + conf.targetname,
597 + '.<br><br><strong>Please confirm you really want to do this.',
598 + '</strong></p>'
599 + ].join('');

Well, I do that all the times so I won't blame you but you will admit this is not very readable and someone editing this code might easily introduce a bug…

a) mu...

Read more...

review: Approve
Revision history for this message
Ian Booth (wallyworld) wrote :
Download full text (6.0 KiB)

Hi Raphaël

Thanks for the great review!

>
> [0]
>
> 1249 lines. Please have mercy on us poor reviewers ;). I know Javascript is verbose and you've done a lot of refactoring but still, I'm sure you would have had a volunteer reviewer for this branch sooner if you had split it into 2 or 3. Also, the problem with enormous branches is that the odds of getting a conflict goes way up.
>

Yeah, the core work was < 800 lines but then the yui tests pushed it
over. Normally I would go and split it but the core work would still
have produced a large diff.

>> 3. Bug found
>> There were conflicting implementations of userCanEditImportance() found. One was essentially:
>> bugtask.userCanEditImportance(self.user) and not bugtask.bugwatch
>
> For instance you could have fixed this in another pipe (I don't know if you already use the bzr pipeline plugin but in case you don't, please check it out: http://wiki.bazaar.canonical.com/BzrPipeline).
>

In fact, this mp itself is the 2nd in a pipeline so I know how to use
such things :-P
The first branch adds the non-ajax ui for deleting bugtasks.
This mp would have been broken without the bug fix; if I were to have
introduced a new pipe before this one, it would only have saved a few lines.

> [2]
>
> 312 + def check_bugtask_data(bugtask, can_delete):
> 313 + self.assertIn(bugtask.id, all_bugtask_data)
>
> Don't you think it would be easier to read if you moved this method outside of the test method?
>

I like the use of an inner method here. The code is not relevant outside
the test method it is in and nicely packages the common checks together
to eliminate duplication.

> [3]
>
> 361 + self.assertEqual(
> 362 + view.request.response.getHeader('content-type'),
> 363 + 'application/json')
>
> 'application/json' should be the first argument, otherwise a failure will be difficult to interpret.
>

Of course. Typo. Thanks for spotting that.

> [4]
>
> 334 + bug = self.factory.makeBug()
> 335 + bugtask = self.factory.makeBugTask(bug=bug)
> 336 + target_name = bugtask.bugtargetdisplayname
> 337 + bugtask_url = canonical_url(bugtask, rootsite='bugs')
>
> And
>
> 370 + bug = self.factory.makeBug()
> 371 + bugtask = self.factory.makeBugTask(bug=bug)
> 372 + target_name = bugtask.bugtargetdisplayname
> 373 + default_bugtask_url = canonical_url(
> 374 + bug.default_bugtask, rootsite='bugs')
>
> Would you mind refactoring that code into a common setup method?
>

Sure. I didn't bother because it was only a few lines but I'll add the
method.

> [5]
>
> 517 + if( bugtask_data.hasOwnProperty(id) ) {
>
> small typos: if( bugtask_data.hasOwnProperty(id) ) { → if (bugtask_data.hasOwnProperty(id)) {
>
> Same here:
>
> 532 + if( link.hasClass('js-action') ) {
> 537 + if( Y.Lang.isFunction(connect_func)) {
> 629 + if( content_type === 'application/json' ) {
>

Old habits. I prefer the above formatting and used it for the past 15
years so am still adjusting to the requirement to put the braces in the
"wrong" place :-/

> [6]
>
> 591 + var delete_te...

Read more...

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/app/widgets/templates/form-picker-macros.pt'
--- lib/lp/app/widgets/templates/form-picker-macros.pt 2011-08-18 08:00:28 +0000
+++ lib/lp/app/widgets/templates/form-picker-macros.pt 2011-11-04 14:06:00 +0000
@@ -40,9 +40,11 @@
40 var vocabulary = config.vocabulary_name;40 var vocabulary = config.vocabulary_name;
41 var vocabulary_filters = config.vocabulary_filters;41 var vocabulary_filters = config.vocabulary_filters;
42 var input_element = config.input_element;42 var input_element = config.input_element;
43 Y.on('domready', function(e) {43 var show_widget_id = '${view/show_widget_id}';
44 var namespace = Y.namespace('lp.app.picker.connect');
45 namespace[show_widget_id] = function() {
44 // Sort out the Choose... link.46 // Sort out the Choose... link.
45 var show_widget_node = Y.one('#${view/show_widget_id}');47 var show_widget_node = Y.one('#'+show_widget_id);
4648
47 show_widget_node.set('innerHTML', 'Choose&hellip;');49 show_widget_node.set('innerHTML', 'Choose&hellip;');
48 show_widget_node.addClass('js-action');50 show_widget_node.addClass('js-action');
@@ -56,6 +58,9 @@
56 picker.show();58 picker.show();
57 e.preventDefault();59 e.preventDefault();
58 });60 });
61 };
62 Y.on('domready', function(e) {
63 namespace[show_widget_id]();
59 });64 });
60 });65 });
61 "/>66 "/>
6267
=== modified file 'lib/lp/bugs/browser/bugtask.py'
--- lib/lp/bugs/browser/bugtask.py 2011-11-04 14:05:57 +0000
+++ lib/lp/bugs/browser/bugtask.py 2011-11-04 14:06:00 +0000
@@ -119,6 +119,7 @@
119 isinstance as zope_isinstance,119 isinstance as zope_isinstance,
120 removeSecurityProxy,120 removeSecurityProxy,
121 )121 )
122from zope.traversing.browser import absoluteURL
122from zope.traversing.interfaces import IPathAdapter123from zope.traversing.interfaces import IPathAdapter
123124
124from canonical.config import config125from canonical.config import config
@@ -678,11 +679,20 @@
678 cancel_url = canonical_url(self.context)679 cancel_url = canonical_url(self.context)
679 return cancel_url680 return cancel_url
680681
682 @cachedproperty
683 def api_request(self):
684 return IWebServiceClientRequest(self.request)
685
681 def initialize(self):686 def initialize(self):
682 """Set up the needed widgets."""687 """Set up the needed widgets."""
683 bug = self.context.bug688 bug = self.context.bug
684 cache = IJSONRequestCache(self.request)689 cache = IJSONRequestCache(self.request)
685 cache.objects['bug'] = bug690 cache.objects['bug'] = bug
691 subscribers_url_data = {
692 'web_link': canonical_url(bug, rootsite='bugs'),
693 'self_link': absoluteURL(bug, self.api_request),
694 }
695 cache.objects['subscribers_portlet_url_data'] = subscribers_url_data
686 cache.objects['total_comments_and_activity'] = (696 cache.objects['total_comments_and_activity'] = (
687 self.total_comments + self.total_activity)697 self.total_comments + self.total_activity)
688 cache.objects['initial_comment_batch_offset'] = (698 cache.objects['initial_comment_batch_offset'] = (
@@ -1771,13 +1781,42 @@
1771 label = 'Remove bug task'1781 label = 'Remove bug task'
1772 page_title = label1782 page_title = label
17731783
1784 @property
1785 def next_url(self):
1786 """Return the next URL to call when this call completes."""
1787 if not self.request.is_ajax:
1788 return super(BugTaskDeletionView, self).next_url
1789 return None
1790
1774 @action('Delete', name='delete_bugtask')1791 @action('Delete', name='delete_bugtask')
1775 def delete_bugtask_action(self, action, data):1792 def delete_bugtask_action(self, action, data):
1776 bugtask = self.context1793 bugtask = self.context
1794 bug = bugtask.bug
1795 deleted_bugtask_url = canonical_url(self.context, rootsite='bugs')
1777 message = ("This bug no longer affects %s."1796 message = ("This bug no longer affects %s."
1778 % bugtask.target.bugtargetdisplayname)1797 % bugtask.bugtargetdisplayname)
1779 bugtask.delete()1798 bugtask.delete()
1780 self.request.response.addNotification(message)1799 self.request.response.addNotification(message)
1800 if self.request.is_ajax:
1801 launchbag = getUtility(ILaunchBag)
1802 launchbag.add(bug.default_bugtask)
1803 # If we are deleting the current highlighted bugtask via ajax,
1804 # we must force a redirect to the new default bugtask to ensure
1805 # all URLs and other client cache content is correctly refreshed.
1806 # We can't do the redirect here since the XHR caller won't see it
1807 # so we return the URL to go to and let the caller do it.
1808 if self._return_url == deleted_bugtask_url:
1809 next_url = canonical_url(
1810 bug.default_bugtask, rootsite='bugs')
1811 self.request.response.setHeader('Content-type',
1812 'application/json')
1813 return dumps(dict(bugtask_url=next_url))
1814 # No redirect required so return the new bugtask table HTML.
1815 view = getMultiAdapter(
1816 (bug, self.request),
1817 name='+bugtasks-and-nominations-table')
1818 view.initialize()
1819 return view.render()
17811820
17821821
1783class BugTaskListingView(LaunchpadView):1822class BugTaskListingView(LaunchpadView):
@@ -3681,6 +3720,10 @@
3681 super(BugTaskTableRowView, self).__init__(context, request)3720 super(BugTaskTableRowView, self).__init__(context, request)
3682 self.milestone_source = MilestoneVocabulary3721 self.milestone_source = MilestoneVocabulary
36833722
3723 @cachedproperty
3724 def api_request(self):
3725 return IWebServiceClientRequest(self.request)
3726
3684 def initialize(self):3727 def initialize(self):
3685 super(BugTaskTableRowView, self).initialize()3728 super(BugTaskTableRowView, self).initialize()
3686 link = canonical_url(self.context)3729 link = canonical_url(self.context)
@@ -3710,15 +3753,23 @@
3710 target_link_title=self.target_link_title,3753 target_link_title=self.target_link_title,
3711 user_can_delete=self.user_can_delete_bugtask,3754 user_can_delete=self.user_can_delete_bugtask,
3712 delete_link=delete_link,3755 delete_link=delete_link,
3713 user_can_edit_importance=self.context.userCanEditImportance(3756 user_can_edit_importance=self.user_can_edit_importance,
3714 self.user),
3715 importance_css_class='importance' + self.context.importance.name,3757 importance_css_class='importance' + self.context.importance.name,
3716 importance_title=self.context.importance.title,3758 importance_title=self.context.importance.title,
3717 # We always look up all milestones, so there's no harm3759 # We always look up all milestones, so there's no harm
3718 # using len on the list here and avoid the COUNT query.3760 # using len on the list here and avoid the COUNT query.
3719 target_has_milestones=len(self._visible_milestones) > 0,3761 target_has_milestones=len(self._visible_milestones) > 0,
3762 user_can_edit_status=self.user_can_edit_status,
3720 )3763 )
37213764
3765 if not self.many_bugtasks:
3766 cache = IJSONRequestCache(self.request)
3767 bugtask_data = cache.objects.get('bugtask_data', None)
3768 if bugtask_data is None:
3769 bugtask_data = dict()
3770 cache.objects['bugtask_data'] = bugtask_data
3771 bugtask_data[bugtask_id] = self.bugtask_config()
3772
3722 def canSeeTaskDetails(self):3773 def canSeeTaskDetails(self):
3723 """Whether someone can see a task's status details.3774 """Whether someone can see a task's status details.
37243775
@@ -3821,7 +3872,7 @@
3821 items = vocabulary_to_choice_edit_items(3872 items = vocabulary_to_choice_edit_items(
3822 self._visible_milestones,3873 self._visible_milestones,
3823 value_fn=lambda item: canonical_url(3874 value_fn=lambda item: canonical_url(
3824 item, request=IWebServiceClientRequest(self.request)))3875 item, request=self.api_request))
3825 items.append({3876 items.append({
3826 "name": "Remove milestone",3877 "name": "Remove milestone",
3827 "disabled": False,3878 "disabled": False,
@@ -3835,13 +3886,29 @@
3835 """Return the canonical url for the bugtask."""3886 """Return the canonical url for the bugtask."""
3836 return canonical_url(self.context)3887 return canonical_url(self.context)
38373888
3838 @property3889 @cachedproperty
3839 def user_can_edit_importance(self):3890 def user_can_edit_importance(self):
3840 """Can the user edit the Importance field?3891 """Can the user edit the Importance field?
38413892
3842 If yes, return True, otherwise return False.3893 If yes, return True, otherwise return False.
3843 """3894 """
3844 return self.context.userCanEditImportance(self.user)3895 bugtask = self.context
3896 return (self.user_can_edit_status
3897 and bugtask.userCanEditImportance(self.user))
3898
3899 @cachedproperty
3900 def user_can_edit_status(self):
3901 """Can the user edit the Status field?
3902
3903 If yes, return True, otherwise return False.
3904 """
3905 bugtask = self.context
3906 edit_allowed = bugtask.target_uses_malone or bugtask.bugwatch
3907 if bugtask.bugwatch:
3908 bugtracker = bugtask.bugwatch.bugtracker
3909 edit_allowed = (
3910 bugtracker.bugtrackertype == BugTrackerType.EMAILADDRESS)
3911 return edit_allowed
38453912
3846 @property3913 @property
3847 def user_can_edit_assignee(self):3914 def user_can_edit_assignee(self):
@@ -3883,8 +3950,8 @@
3883 else:3950 else:
3884 return ''3951 return ''
38853952
3886 def js_config(self):3953 def bugtask_config(self):
3887 """Configuration for the JS widgets on the row, JSON-serialized."""3954 """Configuration for the bugtask JS widgets on the row."""
3888 assignee_vocabulary, assignee_vocabulary_filters = (3955 assignee_vocabulary, assignee_vocabulary_filters = (
3889 get_assignee_vocabulary_info(self.context))3956 get_assignee_vocabulary_info(self.context))
3890 # If we have no filters or just the ALL filter, then no filtering3957 # If we have no filters or just the ALL filter, then no filtering
@@ -3906,16 +3973,21 @@
3906 not self.context.userCanSetAnyAssignee(user) and3973 not self.context.userCanSetAnyAssignee(user) and
3907 (user is None or user.teams_participated_in.count() == 0))3974 (user is None or user.teams_participated_in.count() == 0))
3908 cx = self.context3975 cx = self.context
3909 return dumps(dict(3976 return dict(
3910 row_id=self.data['row_id'],3977 row_id=self.data['row_id'],
3978 form_row_id=self.data['form_row_id'],
3911 bugtask_path='/'.join([''] + self.data['link'].split('/')[3:]),3979 bugtask_path='/'.join([''] + self.data['link'].split('/')[3:]),
3912 prefix=get_prefix(cx),3980 prefix=get_prefix(cx),
3981 targetname=cx.bugtargetdisplayname,
3982 bug_title=cx.bug.title,
3913 assignee_value=cx.assignee and cx.assignee.name,3983 assignee_value=cx.assignee and cx.assignee.name,
3914 assignee_is_team=cx.assignee and cx.assignee.is_team,3984 assignee_is_team=cx.assignee and cx.assignee.is_team,
3915 assignee_vocabulary=assignee_vocabulary,3985 assignee_vocabulary=assignee_vocabulary,
3916 assignee_vocabulary_filters=filter_details,3986 assignee_vocabulary_filters=filter_details,
3917 hide_assignee_team_selection=hide_assignee_team_selection,3987 hide_assignee_team_selection=hide_assignee_team_selection,
3918 user_can_unassign=cx.userCanUnassign(user),3988 user_can_unassign=cx.userCanUnassign(user),
3989 user_can_delete=self.user_can_delete_bugtask,
3990 delete_link=self.data['delete_link'],
3919 target_is_product=IProduct.providedBy(cx.target),3991 target_is_product=IProduct.providedBy(cx.target),
3920 status_widget_items=self.status_widget_items,3992 status_widget_items=self.status_widget_items,
3921 status_value=cx.status.title,3993 status_value=cx.status.title,
@@ -3925,14 +3997,13 @@
3925 milestone_value=(3997 milestone_value=(
3926 canonical_url(3998 canonical_url(
3927 cx.milestone,3999 cx.milestone,
3928 request=IWebServiceClientRequest(self.request))4000 request=self.api_request)
3929 if cx.milestone else None),4001 if cx.milestone else None),
3930 user_can_edit_assignee=self.user_can_edit_assignee,4002 user_can_edit_assignee=self.user_can_edit_assignee,
3931 user_can_edit_milestone=self.user_can_edit_milestone,4003 user_can_edit_milestone=self.user_can_edit_milestone,
3932 user_can_edit_status=not cx.bugwatch,4004 user_can_edit_status=self.user_can_edit_status,
3933 user_can_edit_importance=(4005 user_can_edit_importance=self.user_can_edit_importance,
3934 self.user_can_edit_importance and not cx.bugwatch)4006 )
3935 ))
39364007
39374008
3938class BugsBugTaskSearchListingView(BugTaskSearchListingView):4009class BugsBugTaskSearchListingView(BugTaskSearchListingView):
39394010
=== modified file 'lib/lp/bugs/browser/configure.zcml'
--- lib/lp/bugs/browser/configure.zcml 2011-11-04 14:05:57 +0000
+++ lib/lp/bugs/browser/configure.zcml 2011-11-04 14:06:00 +0000
@@ -1029,6 +1029,12 @@
1029 for="lp.bugs.interfaces.bug.IBug"1029 for="lp.bugs.interfaces.bug.IBug"
1030 class="lp.bugs.browser.bugtask.BugTasksAndNominationsView"1030 class="lp.bugs.browser.bugtask.BugTasksAndNominationsView"
1031 permission="launchpad.View"1031 permission="launchpad.View"
1032 name="+bugtasks-and-nominations-portal"
1033 template="../templates/bugtasks-and-nominations-portal.pt"/>
1034 <browser:page
1035 for="lp.bugs.interfaces.bug.IBug"
1036 class="lp.bugs.browser.bugtask.BugTasksAndNominationsView"
1037 permission="launchpad.View"
1032 name="+bugtasks-and-nominations-table"1038 name="+bugtasks-and-nominations-table"
1033 template="../templates/bugtasks-and-nominations-table.pt"/>1039 template="../templates/bugtasks-and-nominations-table.pt"/>
1034 <browser:page1040 <browser:page
10351041
=== modified file 'lib/lp/bugs/browser/tests/bug-views.txt'
--- lib/lp/bugs/browser/tests/bug-views.txt 2011-08-01 05:25:59 +0000
+++ lib/lp/bugs/browser/tests/bug-views.txt 2011-11-04 14:06:00 +0000
@@ -432,9 +432,20 @@
432BugTasks and Nominations Table432BugTasks and Nominations Table
433------------------------------433------------------------------
434434
435A table is rendered at the top of the bug page which shows both bugtasks435Content is rendered at the top of the bug page which shows both bugtasks
436and nominations. This table is rendered with the436and nominations and various links like "Does this bug affect you" and
437+bugtasks-and-nomination-table view.437"Also Affects Project" etc. This content is rendered with the
438+bugtasks-and-nominations-portal view.
439
440 >>> request = LaunchpadTestRequest()
441
442 >>> bugtasks_and_nominations_view = getMultiAdapter(
443 ... (bug_one_bugtask.bug, request),
444 ... name="+bugtasks-and-nominations-portal")
445 >>> bugtasks_and_nominations_view.initialize()
446
447The bugtasks and nominations table itself is rendered with the
448+bugtasks-and-nominations-table view.
438449
439 >>> request = LaunchpadTestRequest()450 >>> request = LaunchpadTestRequest()
440451
441452
=== modified file 'lib/lp/bugs/browser/tests/test_bug_views.py'
--- lib/lp/bugs/browser/tests/test_bug_views.py 2011-10-25 02:12:44 +0000
+++ lib/lp/bugs/browser/tests/test_bug_views.py 2011-11-04 14:06:00 +0000
@@ -105,7 +105,7 @@
105 email_address = "mark@example.com"105 email_address = "mark@example.com"
106 browser = self.getBrowserForBugWithEmail(106 browser = self.getBrowserForBugWithEmail(
107 email_address, no_login=False)107 email_address, no_login=False)
108 self.assertEqual(6, browser.contents.count(email_address))108 self.assertEqual(7, browser.contents.count(email_address))
109109
110 def test_anonymous_sees_not_email_address(self):110 def test_anonymous_sees_not_email_address(self):
111 """The anonymous user cannot see the email address on the page."""111 """The anonymous user cannot see the email address on the page."""
112112
=== modified file 'lib/lp/bugs/browser/tests/test_bugtask.py'
--- lib/lp/bugs/browser/tests/test_bugtask.py 2011-11-04 14:05:57 +0000
+++ lib/lp/bugs/browser/tests/test_bugtask.py 2011-11-04 14:06:00 +0000
@@ -6,6 +6,7 @@
6from contextlib import contextmanager6from contextlib import contextmanager
7from datetime import datetime7from datetime import datetime
8import re8import re
9import simplejson
9import urllib10import urllib
1011
11from lazr.lifecycle.event import ObjectModifiedEvent12from lazr.lifecycle.event import ObjectModifiedEvent
@@ -118,7 +119,7 @@
118 self.getUserBrowser(url, person_no_teams)119 self.getUserBrowser(url, person_no_teams)
119 # This may seem large: it is; there is easily another 30% fat in120 # This may seem large: it is; there is easily another 30% fat in
120 # there.121 # there.
121 self.assertThat(recorder, HasQueryCount(LessThan(76)))122 self.assertThat(recorder, HasQueryCount(LessThan(84)))
122 count_with_no_teams = recorder.count123 count_with_no_teams = recorder.count
123 # count with many teams124 # count with many teams
124 self.invalidate_caches(task)125 self.invalidate_caches(task)
@@ -134,7 +135,7 @@
134 def test_rendered_query_counts_constant_with_attachments(self):135 def test_rendered_query_counts_constant_with_attachments(self):
135 with celebrity_logged_in('admin'):136 with celebrity_logged_in('admin'):
136 browses_under_limit = BrowsesWithQueryLimit(137 browses_under_limit = BrowsesWithQueryLimit(
137 82, self.factory.makePerson())138 86, self.factory.makePerson())
138139
139 # First test with a single attachment.140 # First test with a single attachment.
140 task = self.factory.makeBugTask()141 task = self.factory.makeBugTask()
@@ -569,7 +570,7 @@
569570
570 request = LaunchpadTestRequest()571 request = LaunchpadTestRequest()
571 foo_bugtasks_and_nominations_view = getMultiAdapter(572 foo_bugtasks_and_nominations_view = getMultiAdapter(
572 (foo_bug, request), name="+bugtasks-and-nominations-table")573 (foo_bug, request), name="+bugtasks-and-nominations-portal")
573 foo_bugtasks_and_nominations_view.initialize()574 foo_bugtasks_and_nominations_view.initialize()
574575
575 task_and_nomination_views = (576 task_and_nomination_views = (
@@ -593,7 +594,7 @@
593594
594 request = LaunchpadTestRequest()595 request = LaunchpadTestRequest()
595 foo_bugtasks_and_nominations_view = getMultiAdapter(596 foo_bugtasks_and_nominations_view = getMultiAdapter(
596 (foo_bug, request), name="+bugtasks-and-nominations-table")597 (foo_bug, request), name="+bugtasks-and-nominations-portal")
597 foo_bugtasks_and_nominations_view.initialize()598 foo_bugtasks_and_nominations_view.initialize()
598599
599 task_and_nomination_views = (600 task_and_nomination_views = (
@@ -689,6 +690,37 @@
689 'bugtask-delete-task%d' % bug.default_bugtask.id)690 'bugtask-delete-task%d' % bug.default_bugtask.id)
690 self.assertIsNone(delete_icon)691 self.assertIsNone(delete_icon)
691692
693 def test_client_cache_contents(self):
694 """ Test that the client cache contains the expected data.
695
696 The cache data is used by the Javascript to enable the delete
697 links to work as expected.
698 """
699 bug = self.factory.makeBug()
700 bugtask_owner = self.factory.makePerson()
701 bugtask = self.factory.makeBugTask(bug=bug, owner=bugtask_owner)
702 with FeatureFixture(DELETE_BUGTASK_ENABLED):
703 login_person(bugtask.owner)
704 getUtility(ILaunchBag).add(bug.default_bugtask)
705 view = create_initialized_view(
706 bug, name='+bugtasks-and-nominations-table',
707 principal=bugtask.owner)
708 view.render()
709 cache = IJSONRequestCache(view.request)
710 all_bugtask_data = cache.objects['bugtask_data']
711
712 def check_bugtask_data(bugtask, can_delete):
713 self.assertIn(bugtask.id, all_bugtask_data)
714 bugtask_data = all_bugtask_data[bugtask.id]
715 self.assertEqual(
716 'task%d' % bugtask.id, bugtask_data['form_row_id'])
717 self.assertEqual(
718 'tasksummary%d' % bugtask.id, bugtask_data['row_id'])
719 self.assertEqual(can_delete, bugtask_data['user_can_delete'])
720
721 check_bugtask_data(bug.default_bugtask, False)
722 check_bugtask_data(bugtask, True)
723
692724
693class TestBugTaskDeleteView(TestCaseWithFactory):725class TestBugTaskDeleteView(TestCaseWithFactory):
694 """Test the bug task delete form."""726 """Test the bug task delete form."""
@@ -734,6 +766,86 @@
734 expected = 'This bug no longer affects %s.' % target_name766 expected = 'This bug no longer affects %s.' % target_name
735 self.assertEqual(expected, notifications[0].message)767 self.assertEqual(expected, notifications[0].message)
736768
769 def _create_bugtask_to_delete(self):
770 bug = self.factory.makeBug()
771 bugtask = self.factory.makeBugTask(bug=bug)
772 target_name = bugtask.bugtargetdisplayname
773 bugtask_url = canonical_url(bugtask, rootsite='bugs')
774 return bug, bugtask, target_name, bugtask_url
775
776 def test_ajax_delete_current_bugtask(self):
777 # Test that deleting the current bugtask returns a JSON dict
778 # containing the URL of the bug's default task to redirect to.
779 bug, bugtask, target_name, bugtask_url = (
780 self._create_bugtask_to_delete())
781 with FeatureFixture(DELETE_BUGTASK_ENABLED):
782 login_person(bugtask.owner)
783 # Set up the request so that we correctly simulate an XHR call
784 # from the URL of the bugtask we are deleting.
785 server_url = canonical_url(
786 getUtility(ILaunchpadRoot), rootsite='bugs')
787 extra = {
788 'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest',
789 'HTTP_REFERER': bugtask_url,
790 }
791 form = {
792 'field.actions.delete_bugtask': 'Delete'
793 }
794 view = create_initialized_view(
795 bugtask, name='+delete', server_url=server_url, form=form,
796 principal=bugtask.owner, **extra)
797 result_data = simplejson.loads(view.render())
798 self.assertEqual([bug.default_bugtask], bug.bugtasks)
799 notifications = simplejson.loads(
800 view.request.response.getHeader('X-Lazr-Notifications'))
801 self.assertEqual(1, len(notifications))
802 expected = 'This bug no longer affects %s.' % target_name
803 self.assertEqual(expected, notifications[0][1])
804 self.assertEqual(
805 'application/json',
806 view.request.response.getHeader('content-type'))
807 expected_url = canonical_url(bug.default_bugtask, rootsite='bugs')
808 self.assertEqual(dict(bugtask_url=expected_url), result_data)
809
810 def test_ajax_delete_non_current_bugtask(self):
811 # Test that deleting the non-current bugtask returns the new bugtasks
812 # table as HTML.
813 bug, bugtask, target_name, bugtask_url = (
814 self._create_bugtask_to_delete())
815 default_bugtask_url = canonical_url(
816 bug.default_bugtask, rootsite='bugs')
817 with FeatureFixture(DELETE_BUGTASK_ENABLED):
818 login_person(bugtask.owner)
819 # Set up the request so that we correctly simulate an XHR call
820 # from the URL of the default bugtask, not the one we are
821 # deleting.
822 server_url = canonical_url(
823 getUtility(ILaunchpadRoot), rootsite='bugs')
824 extra = {
825 'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest',
826 'HTTP_REFERER': default_bugtask_url,
827 }
828 form = {
829 'field.actions.delete_bugtask': 'Delete'
830 }
831 view = create_initialized_view(
832 bugtask, name='+delete', server_url=server_url, form=form,
833 principal=bugtask.owner, **extra)
834 result_html = view.render()
835 self.assertEqual([bug.default_bugtask], bug.bugtasks)
836 notifications = view.request.response.notifications
837 self.assertEqual(1, len(notifications))
838 expected = 'This bug no longer affects %s.' % target_name
839 self.assertEqual(expected, notifications[0].message)
840 self.assertEqual(
841 view.request.response.getHeader('content-type'), 'text/html')
842 table = find_tag_by_id(result_html, 'affected-software')
843 self.assertIsNotNone(table)
844 [row] = table.tbody.findAll('tr', {'class': 'highlight'})
845 target_link = row.find('a', {'class': 'sprite product'})
846 self.assertIn(
847 bug.default_bugtask.bugtargetdisplayname, target_link)
848
737849
738class TestBugTasksAndNominationsViewAlsoAffects(TestCaseWithFactory):850class TestBugTasksAndNominationsViewAlsoAffects(TestCaseWithFactory):
739 """ Tests the boolean methods on the view used to indicate whether the851 """ Tests the boolean methods on the view used to indicate whether the
@@ -752,7 +864,7 @@
752 def _createView(self, bug):864 def _createView(self, bug):
753 request = LaunchpadTestRequest()865 request = LaunchpadTestRequest()
754 bugtasks_and_nominations_view = getMultiAdapter(866 bugtasks_and_nominations_view = getMultiAdapter(
755 (bug, request), name="+bugtasks-and-nominations-table")867 (bug, request), name="+bugtasks-and-nominations-portal")
756 return bugtasks_and_nominations_view868 return bugtasks_and_nominations_view
757869
758 def test_project_bug_cannot_affect_something_else(self):870 def test_project_bug_cannot_affect_something_else(self):
759871
=== modified file 'lib/lp/bugs/javascript/bugtask_index.js'
--- lib/lp/bugs/javascript/bugtask_index.js 2011-10-27 01:11:39 +0000
+++ lib/lp/bugs/javascript/bugtask_index.js 2011-11-04 14:06:00 +0000
@@ -11,6 +11,9 @@
1111
12var namespace = Y.namespace('lp.bugs.bugtask_index');12var namespace = Y.namespace('lp.bugs.bugtask_index');
1313
14// Override for testing
15namespace.ANIM_DURATION = 1;
16
14// lazr.FormOverlay objects.17// lazr.FormOverlay objects.
15var duplicate_form_overlay;18var duplicate_form_overlay;
16var privacy_form_overlay;19var privacy_form_overlay;
@@ -246,7 +249,10 @@
246 dupe_span.one('a').set('href', update_dupe_url);249 dupe_span.one('a').set('href', update_dupe_url);
247 hide_comment_on_duplicate_warning();250 hide_comment_on_duplicate_warning();
248 }251 }
249 Y.lp.anim.green_flash({node: dupe_span}).run();252 Y.lp.anim.green_flash({
253 node: dupe_span,
254 duration: namespace.ANIM_DURATION
255 }).run();
250 // ensure the new link is hooked up correctly:256 // ensure the new link is hooked up correctly:
251 dupe_span.one('a').on(257 dupe_span.one('a').on(
252 'click', function(e){258 'click', function(e){
@@ -355,7 +361,10 @@
355 privacy_link.setStyle('display', 'inline');361 privacy_link.setStyle('display', 'inline');
356 };362 };
357 error_handler.showError = function (error_msg) {363 error_handler.showError = function (error_msg) {
358 Y.lp.anim.red_flash({node: privacy_div}).run();364 Y.lp.anim.red_flash({
365 node: privacy_div,
366 duration: namespace.ANIM_DURATION
367 }).run();
359 privacy_form_overlay.showError(error_msg);368 privacy_form_overlay.showError(error_msg);
360 privacy_form_overlay.show();369 privacy_form_overlay.show();
361 };370 };
@@ -438,7 +447,10 @@
438 }447 }
439 Y.lp.client.display_notifications(448 Y.lp.client.display_notifications(
440 response.getResponseHeader('X-Lazr-Notifications'));449 response.getResponseHeader('X-Lazr-Notifications'));
441 Y.lp.anim.green_flash({node: privacy_div}).run();450 Y.lp.anim.green_flash({
451 node: privacy_div,
452 duration: namespace.ANIM_DURATION
453 }).run();
442 },454 },
443 failure: error_handler.getFailureHandler()455 failure: error_handler.getFailureHandler()
444 }456 }
@@ -578,9 +590,15 @@
578590
579 var bug_branch_container = Y.one('#bug-branches-container');591 var bug_branch_container = Y.one('#bug-branches-container');
580 bug_branch_container.appendChild(bug_branch_list);592 bug_branch_container.appendChild(bug_branch_list);
581 anim = Y.lp.anim.green_flash({node: bug_branch_list});593 anim = Y.lp.anim.green_flash({
594 node: bug_branch_list,
595 duration: namespace.ANIM_DURATION
596 });
582 } else {597 } else {
583 anim = Y.lp.anim.green_flash({node: bug_branch_node});598 anim = Y.lp.anim.green_flash({
599 node: bug_branch_node,
600 duration: namespace.ANIM_DURATION
601 });
584 }602 }
585603
586 var existing_bug_branch_node = bug_branch_list.one(604 var existing_bug_branch_node = bug_branch_list.one(
@@ -591,7 +609,10 @@
591 bug_branch_list.appendChild(bug_branch_node);609 bug_branch_list.appendChild(bug_branch_node);
592 } else {610 } else {
593 // If the bug branch exists already, flash it.611 // If the bug branch exists already, flash it.
594 anim = Y.lp.anim.green_flash({node: existing_bug_branch_node});612 anim = Y.lp.anim.green_flash({
613 node: existing_bug_branch_node,
614 duration: namespace.ANIM_DURATION
615 });
595 }616 }
596 anim.run();617 anim.run();
597 // Fire of the generic branch linked event.618 // Fire of the generic branch linked event.
@@ -624,6 +645,196 @@
624};645};
625646
626/**647/**
648 * Set up the bug task table.
649 *
650 * Called once on load, to initialize the page, and also when the contents of
651 * the bug task table is replaced after an XHR call.
652 *
653 * @method setup_bugtask_table
654 */
655namespace.setup_bugtask_table = function() {
656 var bugtask_data = LP.cache.bugtask_data;
657 if (!Y.Lang.isValue(bugtask_data)) {
658 return;
659 }
660 var picker_connect = Y.namespace('lp.app.picker.connect');
661 var process_link = function(link) {
662 // The link may already have been processed.
663 if (link.hasClass('js-action')) {
664 return;
665 }
666 var func_name = link.get('id');
667 var connect_func = picker_connect[func_name];
668 if (Y.Lang.isFunction(connect_func)) {
669 connect_func();
670 }
671 };
672 var id;
673 for (id in bugtask_data) {
674 if (bugtask_data.hasOwnProperty(id)) {
675 var conf = bugtask_data[id];
676 // We need to wire the target and assignee pickers in the
677 // expandable bugtask edit form. This setup_bugtask_table() method
678 // is called when the page loads as well as after replacing the
679 // table. On page load, the pickers are wired by javascript
680 // embedded in the picker tales so we need to ensure we handle
681 // this case.
682 var tr = Y.one('#' + conf.form_row_id);
683 if (tr === null) {
684 //The row has been deleted.
685 continue;
686 }
687 tr.all('a').each(process_link);
688 // Now wire up the javascript widgets in the table row.
689 namespace.setup_bugtask_row(conf);
690 }
691 }
692};
693
694/**
695 * Show a spinner next to the delete icon.
696 *
697 * @method _showDeleteSpinner
698 */
699namespace._showDeleteSpinner = function(delete_link) {
700 var spinner_node = Y.Node.create(
701 '<img class="spinner" src="/@@/spinner" alt="Deleting..." />');
702 delete_link.insertBefore(spinner_node, delete_link);
703 delete_link.addClass('unseen');
704};
705
706/**
707 * Hide the delete spinner.
708 *
709 * @method _hideDeleteSpinner
710 */
711namespace._hideDeleteSpinner = function(delete_link) {
712 delete_link.removeClass('unseen');
713 var spinner = delete_link.get('parentNode').one('.spinner');
714 if (spinner !== null) {
715 spinner.remove();
716 }
717};
718
719/**
720 * Replace the currect bugtask table with a new one, ensuring all Javascript
721 * widgets are correctly wired up.
722 *
723 * @method _render_bugtask_table
724 */
725namespace._render_bugtask_table = function(new_table) {
726 var bugtask_table = Y.one('#affected-software');
727 bugtask_table.replace(new_table);
728 namespace.setup_bugtask_table();
729};
730
731/**
732 * Prompt the user to confirm the deletion of the selected bugtask.
733 * widgets are correctly wired up.
734 *
735 * @method _confirm_bugtask_delete
736 */
737namespace._confirm_bugtask_delete = function(delete_link, conf) {
738 var delete_text_template = [
739 '<p class="large-warning" style="padding:2px 2px 0 36px;">',
740 ' You are about to mark bug "{bug}"<br>as no longer affecting',
741 ' {target}.<br><br>',
742 ' <strong>Please confirm you really want to do this.</strong>',
743 '</p>'
744 ].join('');
745 var delete_text = Y.Lang.substitute(delete_text_template,
746 {bug: conf.bug_title, target: conf.targetname});
747 var co = new Y.lp.app.confirmationoverlay.ConfirmationOverlay({
748 submit_fn: function() {
749 namespace.delete_bugtask(delete_link, conf);
750 },
751 form_content: delete_text,
752 headerContent: '<h2>Confirm bugtask deletion</h2>'
753 });
754 co.show();
755};
756
757/**
758 * Redirect to a new URL. We need to break this out to allow testing.
759 *
760 * @method _redirect
761 */
762namespace._redirect = function(url) {
763 window.location.replace(url);
764};
765
766/**
767 * Process the result of the XHR request to delete a bugtask.
768 *
769 * @method _process_bugtask_delete_response
770 */
771namespace._process_bugtask_delete_response = function(response, row_id) {
772 // If the result is json, then we need to perform a redirect to a new
773 // bugtask URL. This happens when the current bugtask is deleted and we
774 // need to ensure all link URLS are correctly reset.
775 var content_type = response.getResponseHeader('Content-type');
776 if (content_type === 'application/json') {
777 Y.lp.client.display_notifications(
778 response.getResponseHeader('X-Lazr-Notifications'));
779 var redirect = Y.JSON.parse(response.responseText);
780 Y.lp.anim.red_flash({
781 node: '#' + row_id,
782 duration: namespace.ANIM_DURATION
783 }).run();
784 namespace._redirect(redirect.bugtask_url);
785 return;
786 }
787 // We have received HTML, so we replace the current bugtask table with a
788 // new one.
789 var anim = Y.lp.anim.red_flash({
790 node: '#' + row_id,
791 duration: namespace.ANIM_DURATION
792 });
793 anim.on('end', function() {
794 namespace._render_bugtask_table(response.responseText);
795 Y.lp.client.display_notifications(
796 response.getResponseHeader('X-Lazr-Notifications'));
797 });
798 anim.run();
799};
800
801/**
802 * Delete the bugtask defined by the delete_link using an XHR call.
803 *
804 * @method delete_bugtask
805 */
806namespace.delete_bugtask = function (delete_link, conf) {
807 Y.lp.client.remove_notifications();
808 var error_handler = new Y.lp.client.ErrorHandler();
809 error_handler.showError = Y.bind(function (error_msg) {
810 namespace._hideDeleteSpinner(delete_link);
811 Y.lp.app.errors.display_error(undefined, error_msg);
812 }, this);
813
814 var submit_url = delete_link.get('href');
815 var qs = Y.lp.client.append_qs(
816 '', 'field.actions.delete_bugtask', 'Delete');
817 var y_config = {
818 method: "POST",
819 headers: {'Accept': 'application/json; application/xhtml'},
820 on: {
821 start:
822 Y.bind(namespace._showDeleteSpinner, namespace, delete_link),
823 failure:
824 error_handler.getFailureHandler(),
825 success:
826 function(id, response) {
827 namespace._process_bugtask_delete_response(
828 response, conf.row_id);
829 }
830 },
831 data: qs
832 };
833 var io_provider = Y.lp.client.get_configured_io_provider(conf);
834 io_provider.io(submit_url, y_config);
835};
836
837/**
627 * Set up a bug task table row.838 * Set up a bug task table row.
628 *839 *
629 * Called once per row, on load, to initialize the page.840 * Called once per row, on load, to initialize the page.
@@ -641,6 +852,15 @@
641 var importance_content = tr.one('.importance-content');852 var importance_content = tr.one('.importance-content');
642 var assignee_content = Y.one('#assignee-picker-' + conf.row_id);853 var assignee_content = Y.one('#assignee-picker-' + conf.row_id);
643 var milestone_content = tr.one('.milestone-content');854 var milestone_content = tr.one('.milestone-content');
855 var delete_link = tr.one('.bugtask-delete');
856
857 if (Y.Lang.isValue(LP.links.me) && Y.Lang.isValue(delete_link)
858 && conf.user_can_delete) {
859 delete_link.on('click', function (e) {
860 e.preventDefault();
861 namespace._confirm_bugtask_delete(delete_link, conf);
862 });
863 }
644864
645 if (status_content === null) {865 if (status_content === null) {
646 // Not all table rows have status widgets. If this is one of those866 // Not all table rows have status widgets. If this is one of those
@@ -894,6 +1114,19 @@
894 }1114 }
895 assignee_picker.render();1115 assignee_picker.render();
896 }1116 }
1117
1118 // Set-up the expander on the bug task summary row.
1119 var icon_node = Y.one('tr#' + conf.row_id + ' a.bugtask-expander');
1120 var row_node = Y.one('tr#' + conf.form_row_id);
1121 if (Y.Lang.isValue(row_node)) {
1122 // When no row is present, this is bug task on a project with
1123 // multiple per-series tasks, so we do not need to set
1124 // the expander for the descriptive parent project task.
1125 var content_node = row_node.one('td form');
1126 var expander = new Y.lp.app.widgets.expander.Expander(
1127 icon_node, row_node, { animate_node: content_node });
1128 expander.setUp();
1129 }
897};1130};
8981131
899/**1132/**
@@ -1122,7 +1355,8 @@
1122 comments_container.appendChild(new_comments_node);1355 comments_container.appendChild(new_comments_node);
1123 if (Y.Lang.isValue(Y.lp.anim)) {1356 if (Y.Lang.isValue(Y.lp.anim)) {
1124 var success_anim = Y.lp.anim.green_flash(1357 var success_anim = Y.lp.anim.green_flash(
1125 {node: new_comments_node});1358 {node: new_comments_node,
1359 duration: namespace.ANIM_DURATION});
1126 success_anim.run();1360 success_anim.run();
1127 }1361 }
1128 batch_url_div = Y.one('#next-batch-url');1362 batch_url_div = Y.one('#next-batch-url');
@@ -1209,6 +1443,6 @@
1209 "lazr.formoverlay", "lp.anim", "lazr.base",1443 "lazr.formoverlay", "lp.anim", "lazr.base",
1210 "lazr.overlay", "lazr.choiceedit", "lp.app.picker",1444 "lazr.overlay", "lazr.choiceedit", "lp.app.picker",
1211 "lp.bugs.bugtask_index.portlets.subscription",1445 "lp.bugs.bugtask_index.portlets.subscription",
1212 "lp.client", "escape",1446 "lp.app.widgets.expander", "lp.client", "escape",
1213 "lp.client.plugins", "lp.app.errors",1447 "lp.client.plugins", "lp.app.errors",
1214 "lp.app.privacy"]});1448 "lp.app.privacy", "lp.app.confirmationoverlay"]});
12151449
=== modified file 'lib/lp/bugs/javascript/subscribers.js'
--- lib/lp/bugs/javascript/subscribers.js 2011-07-25 04:32:49 +0000
+++ lib/lp/bugs/javascript/subscribers.js 2011-11-04 14:06:00 +0000
@@ -41,9 +41,14 @@
41 * a relative URI to load subscribers' details from.41 * a relative URI to load subscribers' details from.
42 */42 */
43function createBugSubscribersLoader(config) {43function createBugSubscribersLoader(config) {
44 var url_data = LP.cache.subscribers_portlet_url_data;
45 if (!Y.Lang.isValue(url_data)) {
46 url_data = { self_link: LP.cache.context.bug_link,
47 web_link: LP.cache.context.web_link };
48 }
44 config.subscriber_levels = subscriber_levels;49 config.subscriber_levels = subscriber_levels;
45 config.subscriber_level_order = subscriber_level_order;50 config.subscriber_level_order = subscriber_level_order;
46 config.context = config.bug;51 config.context = url_data;
47 config.subscribe_someone_else_level = 'Discussion';52 config.subscribe_someone_else_level = 'Discussion';
48 config.default_subscriber_level = 'Maybe';53 config.default_subscriber_level = 'Maybe';
49 var module = Y.lp.app.subscribers.subscribers_list;54 var module = Y.lp.app.subscribers.subscribers_list;
5055
=== added file 'lib/lp/bugs/javascript/tests/test_bugtask_delete.html'
--- lib/lp/bugs/javascript/tests/test_bugtask_delete.html 1970-01-01 00:00:00 +0000
+++ lib/lp/bugs/javascript/tests/test_bugtask_delete.html 2011-11-04 14:06:00 +0000
@@ -0,0 +1,81 @@
1<html>
2 <head>
3 <title>Bug task deletion</title>
4
5 <!-- YUI and test setup -->
6 <script type="text/javascript"
7 src="../../../../canonical/launchpad/icing/yui/yui/yui.js">
8 </script>
9 <link rel="stylesheet" href="../../../app/javascript/testing/test.css" />
10 <script type="text/javascript"
11 src="../../../app/javascript/testing/testrunner.js"></script>
12
13 <script type="text/javascript" src="../../../app/javascript/client.js"></script>
14 <script type="text/javascript" src="../../../app/javascript/errors.js"></script>
15 <script type="text/javascript" src="../../../app/javascript/lp.js"></script>
16
17 <!-- Other dependencies -->
18 <script type="text/javascript" src="../../../app/javascript/testing/mockio.js"></script>
19 <script type="text/javascript"
20 src="../../../contrib/javascript/mustache.js"></script>
21 <script type="text/javascript" src="../../../app/javascript/activator/activator.js"></script>
22 <script type="text/javascript" src="../../../app/javascript/anim/anim.js"></script>
23 <script type="text/javascript" src="../../../app/javascript/confirmationoverlay/confirmationoverlay.js"></script>
24 <script type="text/javascript" src="../../../app/javascript/choiceedit/choiceedit.js"></script>
25 <script type="text/javascript" src="../../../app/javascript/effects/effects.js"></script>
26 <script type="text/javascript" src="../../../app/javascript/expander.js"></script>
27 <script type="text/javascript" src="../../../app/javascript/extras/extras.js"></script>
28 <script type="text/javascript" src="../../../app/javascript/formoverlay/formoverlay.js"></script>
29 <script type="text/javascript" src="../../../app/javascript/inlineedit/editor.js"></script>
30 <script type="text/javascript" src="../../../app/javascript/lazr/lazr.js"></script>
31 <script type="text/javascript" src="../../../app/javascript/overlay/overlay.js"></script>
32 <script type="text/javascript" src="../../../app/javascript/picker/picker.js"></script>
33 <script type="text/javascript" src="../../../app/javascript/picker/picker_patcher.js"></script>
34 <script type="text/javascript" src="../../../app/javascript/picker/person_picker.js"></script>
35 <script type="text/javascript" src="../../../app/javascript/privacy.js"></script>
36 <script type="text/javascript" src="../bug_subscription_portlet.js"></script>
37
38 <!-- The module under test -->
39 <script type="text/javascript"
40 src="../bugtask_index.js"></script>
41
42 <!-- The test suite -->
43 <script type="text/javascript"
44 src="test_bugtask_delete.js"></script>
45
46 <!-- Pretty up the sample html -->
47 <style type="text/css">
48 div#sample {margin:15px; width:200px; border:1px solid #999; padding:10px;}
49 </style>
50 </head>
51 <body class="yui3-skin-sam">
52 <div id="fixture"></div>
53 <div id="request-notifications"></div>
54 <script type="text/x-template" id="form-template">
55 <table id="affected-software">
56 <tbody>
57 <tr id="tasksummary49">
58 <td>
59 <div>
60 <a class="sprite product" href="#">Product</a>
61 <button class="lazr-btn yui3-activator-act">
62 Edit
63 </button>
64 <a id="bugtask-delete-task49" href="http://foo"
65 class="sprite remove bugtask-delete"></a>
66 </div>
67 </td>
68 <td>
69 <table>
70 <tr id="task49"><td>
71 <a id="show-widget-product" class="sprite product"
72 href="#">Product</a>
73 </td></tr>
74 </table>
75 </td>
76 </tr>
77 </tbody>
78 </table>
79 </script>
80 </body>
81</html>
082
=== added file 'lib/lp/bugs/javascript/tests/test_bugtask_delete.js'
--- lib/lp/bugs/javascript/tests/test_bugtask_delete.js 1970-01-01 00:00:00 +0000
+++ lib/lp/bugs/javascript/tests/test_bugtask_delete.js 2011-11-04 14:06:00 +0000
@@ -0,0 +1,223 @@
1YUI().use('lp.testing.runner', 'lp.testing.mockio', 'base', 'test', 'console',
2 'node', 'node-event-simulate', 'lp.bugs.bugtask_index',
3 function(Y) {
4
5var suite = new Y.Test.Suite("Bugtask deletion Tests");
6var module = Y.lp.bugs.bugtask_index;
7
8
9suite.add(new Y.Test.Case({
10 name: 'Bugtask delete',
11
12 setUp: function() {
13 module.ANIM_DURATION = 0;
14 this.link_conf = {
15 row_id: 'tasksummary49',
16 form_row_id: 'tasksummary49',
17 user_can_delete: true
18 };
19 window.LP = {
20 links: {me : "/~user"},
21 cache: {
22 bugtask_data: {49: this.link_conf}
23 }
24 };
25 this.fixture = Y.one('#fixture');
26 var bugtasks_table = Y.Node.create(
27 Y.one('#form-template').getContent());
28 this.fixture.appendChild(bugtasks_table);
29 this.delete_link = bugtasks_table.one('#bugtask-delete-task49');
30 },
31
32 tearDown: function() {
33 if (this.fixture !== null) {
34 this.fixture.empty();
35 }
36 Y.one('#request-notifications').empty();
37 delete this.fixture;
38 delete window.LP;
39 },
40
41 test_show_spinner: function() {
42 // Test the delete progress spinner is shown.
43 module._showDeleteSpinner(this.delete_link);
44 Y.Assert.isNotNull(this.fixture.one('.spinner'));
45 Y.Assert.isTrue(this.delete_link.hasClass('unseen'));
46 },
47
48 test_hide_spinner: function() {
49 // Test the delete progress spinner is hidden.
50 module._showDeleteSpinner(this.delete_link);
51 module._hideDeleteSpinner(this.delete_link);
52 Y.Assert.isNull(this.fixture.one('.spinner'));
53 Y.Assert.isFalse(this.delete_link.hasClass('unseen'));
54 },
55
56 _test_delete_confirmation: function(click_ok) {
57 // Test the delete confirmation dialog when delete is clicked.
58 var orig_delete_bugtask = module.delete_bugtask;
59
60 var delete_called = false;
61 var self = this;
62 module.delete_bugtask = function(delete_link, conf) {
63 Y.Assert.areEqual(self.delete_link, delete_link);
64 Y.Assert.areEqual(self.link_conf, conf);
65 delete_called = true;
66 };
67 module.setup_bugtask_table();
68 this.delete_link.simulate('click');
69 var co = Y.one('.yui3-overlay.yui3-lp-app-confirmationoverlay');
70 var actions = co.one('.yui3-lazr-formoverlay-actions');
71 var btn_style;
72 if (click_ok) {
73 btn_style = '.ok-btn';
74 } else {
75 btn_style = '.cancel-btn';
76 }
77 var button = actions.one(btn_style);
78 button.simulate('click');
79 Y.Assert.areEqual(click_ok, delete_called);
80 Y.Assert.isTrue(
81 co.hasClass('yui3-lp-app-confirmationoverlay-hidden'));
82 module.delete_bugtask = orig_delete_bugtask;
83 },
84
85 test_delete_confirmation_ok: function() {
86 // Test the delete confirmation dialog Ok functionality.
87 this._test_delete_confirmation(true);
88 },
89
90 test_delete_confirmation_cancel: function() {
91 // Test the delete confirmation dialog Cancel functionality.
92 this._test_delete_confirmation(false);
93 },
94
95 test_setup_bugtask_table: function() {
96 // Test that the bugtask table is wired up, the pickers and the
97 // delete links etc.
98 var namespace = Y.namespace('lp.app.picker.connect');
99 var connect_picker_called = false;
100 namespace['show-widget-product'] = function() {
101 connect_picker_called = true;
102 };
103 var orig_confirm_bugtask_delete = module._confirm_bugtask_delete;
104 var self = this;
105 var confirm_delete_called = false;
106 module._confirm_bugtask_delete = function(delete_link, conf) {
107 Y.Assert.areEqual(self.delete_link, delete_link);
108 Y.Assert.areEqual(self.link_conf, conf);
109 confirm_delete_called = true;
110 };
111 module.setup_bugtask_table();
112 this.delete_link.simulate('click');
113 Y.Assert.isTrue(connect_picker_called);
114 Y.Assert.isTrue(confirm_delete_called);
115 module._confirm_bugtask_delete = orig_confirm_bugtask_delete;
116 },
117
118 test_render_bugtask_table: function() {
119 // Test that a new bug task table is rendered and setup.
120 var orig_setup_bugtask_table = module.setup_bugtask_table;
121 var setup_called = false;
122 module.setup_bugtask_table = function() {
123 setup_called = true;
124 };
125 var test_table =
126 '<table id="affected-software">'+
127 '<tr><td>foo</td></tr></table>';
128 module._render_bugtask_table(test_table);
129 Y.Assert.isTrue(setup_called);
130 Y.Assert.areEqual(
131 '<tbody><tr><td>foo</td></tr></tbody>',
132 this.fixture.one('table#affected-software').getContent());
133 module.setup_bugtask_table = orig_setup_bugtask_table;
134 },
135
136 test_process_bugtask_delete_redirect_response: function() {
137 // Test the processing of a XHR delete result which is to
138 // redirect the browser to a new URL.
139 var orig_redirect = module._redirect;
140 var redirect_called = false;
141 module._redirect = function(url) {
142 Y.Assert.areEqual('http://foo', url);
143 redirect_called = true;
144 };
145 var response = new Y.lp.testing.mockio.MockHttpResponse({
146 responseText: '{"bugtask_url": "http://foo"}',
147 responseHeaders: {'Content-type': 'application/json'}});
148 module._process_bugtask_delete_response(
149 response, this.link_conf.row_id);
150 this.wait(function() {
151 // Wait for the animation to complete.
152 Y.Assert.isTrue(redirect_called);
153 }, 50);
154 module._redirect = orig_redirect;
155 },
156
157 test_process_bugtask_delete_new_table_response: function() {
158 // Test the processing of a XHR delete result which is to
159 // replace the current bugtasks table.
160 var orig_render_bugtask_table = module._render_bugtask_table;
161 var render_table_called = false;
162 module._render_bugtask_table = function(new_table) {
163 Y.Assert.areEqual('<table>Foo</table>', new_table);
164 render_table_called = true;
165 };
166 var notifications = '[ [20, "Delete Success"] ]';
167 var response = new Y.lp.testing.mockio.MockHttpResponse({
168 responseText: '<table>Foo</table>',
169 responseHeaders: {
170 'Content-type': 'text/html',
171 'X-Lazr-Notifications': notifications}});
172 module._process_bugtask_delete_response(
173 response, this.link_conf.row_id);
174 this.wait(function() {
175 // Wait for the animation to complete.
176 Y.Assert.isTrue(render_table_called);
177 var node = Y.one('div#request-notifications ' +
178 'div.informational.message');
179 Y.Assert.areEqual('Delete Success', node.getContent());
180 }, 50);
181 module._render_bugtask_table = orig_render_bugtask_table;
182 },
183
184 test_delete_bugtask: function() {
185 // Test that when delete_bugtask is called, the expected XHR call
186 // is made.
187 var orig_delete_repsonse =
188 module._process_bugtask_delete_response;
189
190 var delete_response_called = false;
191 var self = this;
192 module._process_bugtask_delete_response = function(response, id) {
193 Y.Assert.areEqual('<p>Foo</p>', response.responseText);
194 Y.Assert.areEqual(self.link_conf.row_id, id);
195 delete_response_called = true;
196 };
197
198 var mockio = new Y.lp.testing.mockio.MockIo();
199 var conf = Y.merge(this.link_conf, {io_provider: mockio});
200 module.delete_bugtask(this.delete_link, conf);
201 mockio.success({
202 responseText: '<p>Foo</p>',
203 responseHeaders: {'Content-Type': 'text/html'}});
204 // Check the parameters passed to the io call.
205 Y.Assert.areEqual(
206 this.delete_link.get('href'),
207 mockio.last_request.url);
208 Y.Assert.areEqual(
209 'POST', mockio.last_request.config.method);
210 Y.Assert.areEqual(
211 'application/json; application/xhtml',
212 mockio.last_request.config.headers.Accept);
213 Y.Assert.areEqual(
214 'field.actions.delete_bugtask=Delete',
215 mockio.last_request.config.data);
216 Y.Assert.isTrue(delete_response_called);
217
218 module._process_bugtask_delete_response = orig_delete_repsonse;
219 }
220}));
221
222Y.lp.testing.Runner.run(suite);
223});
0224
=== modified file 'lib/lp/bugs/javascript/tests/test_subscribers.js'
--- lib/lp/bugs/javascript/tests/test_subscribers.js 2011-07-25 04:32:49 +0000
+++ lib/lp/bugs/javascript/tests/test_subscribers.js 2011-11-04 14:06:00 +0000
@@ -12,19 +12,26 @@
12 setUp: function() {12 setUp: function() {
13 this.root = Y.Node.create('<div />');13 this.root = Y.Node.create('<div />');
14 Y.one('body').appendChild(this.root);14 Y.one('body').appendChild(this.root);
15 window.LP = {
16 cache: {
17 context: {
18 bug_link: '/bug/1',
19 web_link: '/base'
20 }
21 }
22 };
15 },23 },
1624
17 tearDown: function() {25 tearDown: function() {
18 this.root.remove();26 this.root.remove();
27 delete window.LP;
19 },28 },
2029
21 setUpLoader: function() {30 setUpLoader: function() {
22 this.root.appendChild(31 this.root.appendChild(
23 Y.Node.create('<div />').addClass('container'));32 Y.Node.create('<div />').addClass('container'));
24 var bug = { web_link: '/base', self_link: '/bug/1'};
25 return new module.createBugSubscribersLoader({33 return new module.createBugSubscribersLoader({
26 container_box: '.container',34 container_box: '.container',
27 bug: bug,
28 subscribers_details_view: '/+bug-portlet-subscribers-details'});35 subscribers_details_view: '/+bug-portlet-subscribers-details'});
29 },36 },
3037
3138
=== modified file 'lib/lp/bugs/stories/bugs/xx-bug-index.txt'
--- lib/lp/bugs/stories/bugs/xx-bug-index.txt 2011-07-01 15:12:32 +0000
+++ lib/lp/bugs/stories/bugs/xx-bug-index.txt 2011-11-04 14:06:00 +0000
@@ -147,11 +147,11 @@
147 >>> bug_url = canonical_url(bug)147 >>> bug_url = canonical_url(bug)
148 >>> logout()148 >>> logout()
149149
150On the bug page, for every bug task there's one coresponding init script.150On the bug page, for every bug task there's one expander.
151 151
152 >>> browser.open(bug_url)152 >>> browser.open(bug_url)
153 >>> print len(find_tags_by_class(153 >>> print len(find_tags_by_class(
154 ... browser.contents, 'bugtasks-table-row-init-script'))154 ... browser.contents, 'bugtask-expander'))
155 1155 1
156156
157We add four more tasks.157We add four more tasks.
@@ -161,14 +161,14 @@
161 ... _ = bug.addTask(bug.owner, factory.makeProduct())161 ... _ = bug.addTask(bug.owner, factory.makeProduct())
162 >>> logout()162 >>> logout()
163163
164And the init script appears five times.164And the expander appears five times.
165165
166 >>> browser.open(bug_url)166 >>> browser.open(bug_url)
167 >>> print len(find_tags_by_class(167 >>> print len(find_tags_by_class(
168 ... browser.contents, 'bugtasks-table-row-init-script'))168 ... browser.contents, 'bugtask-expander'))
169 5169 5
170170
171But on pages with more than ten bug tasks, we don't include the init scripts171But on pages with more than ten bug tasks, we don't include the expander
172at all.172at all.
173173
174 >>> login('foo.bar@canonical.com')174 >>> login('foo.bar@canonical.com')
@@ -178,6 +178,6 @@
178178
179 >>> browser.open(bug_url)179 >>> browser.open(bug_url)
180 >>> print len(find_tags_by_class(180 >>> print len(find_tags_by_class(
181 ... browser.contents, 'bugtasks-table-row-init-script'))181 ... browser.contents, 'bugtask-expander'))
182 0182 0
183183
184184
=== modified file 'lib/lp/bugs/stories/bugtask-management/xx-bugtask-edit-forms.txt'
--- lib/lp/bugs/stories/bugtask-management/xx-bugtask-edit-forms.txt 2011-07-26 06:22:41 +0000
+++ lib/lp/bugs/stories/bugtask-management/xx-bugtask-edit-forms.txt 2011-11-04 14:06:00 +0000
@@ -26,9 +26,13 @@
26http://bugs.launchpad.dev/firefox/+bug/1/+editstatus26http://bugs.launchpad.dev/firefox/+bug/1/+editstatus
2727
28>>> print admin_browser.getLink('Confirmed').url28>>> print admin_browser.getLink('Confirmed').url
29http://bugs.launchpad.dev/debian/+source/mozilla-firefox/+bug/1/+editstatus29Traceback (most recent call last):
30...
31LinkNotFoundError
30>>> print admin_browser.getLink('Low', index=1).url32>>> print admin_browser.getLink('Low', index=1).url
31http://bugs.launchpad.dev/debian/+source/mozilla-firefox/+bug/1/+editstatus33Traceback (most recent call last):
34...
35LinkNotFoundError
3236
33>>> print admin_browser.getLink('New', index=1).url37>>> print admin_browser.getLink('New', index=1).url
34http://bugs.launchpad.dev/ubuntu/+source/mozilla-firefox/+bug/1/+editstatus38http://bugs.launchpad.dev/ubuntu/+source/mozilla-firefox/+bug/1/+editstatus
3539
=== modified file 'lib/lp/bugs/templates/bugtask-index.pt'
--- lib/lp/bugs/templates/bugtask-index.pt 2011-10-06 18:51:36 +0000
+++ lib/lp/bugs/templates/bugtask-index.pt 2011-11-04 14:06:00 +0000
@@ -20,13 +20,11 @@
20 Y.lp.code.branchmergeproposal.diff.connect_diff_links();20 Y.lp.code.branchmergeproposal.diff.connect_diff_links();
21 }, window);21 }, window);
22 Y.on('domready', function() {22 Y.on('domready', function() {
23 var bug = { self_link: LP.cache.context.bug_link,23 Y.lp.bugs.bugtask_index.setup_bugtask_table();
24 web_link: LP.cache.context.web_link };
25 LP.cache.comment_context = LP.cache.bug;24 LP.cache.comment_context = LP.cache.bug;
26 Y.lp.comments.hide.setup_hide_controls();25 Y.lp.comments.hide.setup_hide_controls();
27 var sl = new Y.lp.bugs.subscribers.createBugSubscribersLoader({26 var sl = new Y.lp.bugs.subscribers.createBugSubscribersLoader({
28 container_box: '#other-bug-subscribers',27 container_box: '#other-bug-subscribers',
29 bug: bug,
30 subscribers_details_view:28 subscribers_details_view:
31 '/+bug-portlet-subscribers-details',29 '/+bug-portlet-subscribers-details',
32 subscribe_someone_else_link: '.menu-link-addsubscriber'30 subscribe_someone_else_link: '.menu-link-addsubscriber'
@@ -138,8 +136,7 @@
138 <tal:heat replace="structure view/bug_heat_html" />136 <tal:heat replace="structure view/bug_heat_html" />
139 </div>137 </div>
140138
141 <div tal:replace="structure context/bug/@@+bugtasks-and-nominations-table" />139 <div tal:replace="structure context/bug/@@+bugtasks-and-nominations-portal" />
142
143 <div id="maincontentsub">140 <div id="maincontentsub">
144 <div><!-- id="nonportlets"> -->141 <div><!-- id="nonportlets"> -->
145 <div class="top-portlet">142 <div class="top-portlet">
146143
=== 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-11-04 14:05:57 +0000
+++ lib/lp/bugs/templates/bugtask-tasks-and-nominations-table-row.pt 2011-11-04 14:06:00 +0000
@@ -62,15 +62,21 @@
62 <div class="status-content"62 <div class="status-content"
63 style="width: 100%; float: left"63 style="width: 100%; float: left"
64 tal:define="status context/status">64 tal:define="status context/status">
65 <a href="+editstatus"65 <span tal:condition="not: data/user_can_edit_status"
66 tal:attributes="class string:value status${status/name};66 style="float: left"
67 href data/edit_link"67 tal:attributes="class string:value status${status/name};"
68 style="float: left"68 tal:content="status/title"/>
69 tal:content="status/title" />69 <tal:edit-status tal:condition="data/user_can_edit_status">
70 <a href="+editstatus" style="margin-left: 3px"70 <a href="+editstatus"
71 tal:attributes="href data/edit_link">71 tal:attributes="class string:value status${status/name};
72 <img class="editicon" src="/@@/edit" />72 href data/edit_link"
73 </a>73 style="float: left"
74 tal:content="status/title" />
75 <a href="+editstatus" style="margin-left: 3px"
76 tal:attributes="href data/edit_link">
77 <img class="editicon" src="/@@/edit" />
78 </a>
79 </tal:edit-status>
74 </div>80 </div>
75 </td>81 </td>
7682
@@ -177,30 +183,6 @@
177183
178 </tal:not-conjoined-task>184 </tal:not-conjoined-task>
179 </tr>185 </tr>
180 <script type="text/javascript"
181 class="bugtasks-table-row-init-script"
182 tal:condition="not:view/many_bugtasks"
183 tal:content="string:
184 LPS.use('event', 'lp.bugs.bugtask_index', 'lp.app.widgets.expander',
185 function(Y) {
186 Y.on('domready', function() {
187 Y.lp.bugs.bugtask_index.setup_bugtask_row(${view/js_config});
188
189 // Set-up the expander on the bug task summary row.
190 var icon_node = Y.one('tr#${data/row_id} a.bugtask-expander');
191 var row_node = Y.one('tr#${data/form_row_id}');
192 if (Y.Lang.isValue(row_node)) {
193 // When no row is present, this is bug task on a project with
194 // multiple per-series tasks, so we do not need to set
195 // the expander for the descriptive parent project task.
196 var content_node = row_node.one('td form');
197 var expander = new Y.lp.app.widgets.expander.Expander(
198 icon_node, row_node, { animate_node: content_node });
199 expander.setUp();
200 }
201 });
202 });
203 "/>
204186
205 <tal:form condition="view/displayEditForm">187 <tal:form condition="view/displayEditForm">
206 <tr188 <tr
207189
=== renamed file 'lib/lp/bugs/templates/bugtasks-and-nominations-table.pt' => 'lib/lp/bugs/templates/bugtasks-and-nominations-portal.pt'
--- lib/lp/bugs/templates/bugtasks-and-nominations-table.pt 2011-10-26 03:58:48 +0000
+++ lib/lp/bugs/templates/bugtasks-and-nominations-portal.pt 2011-11-04 14:06:00 +0000
@@ -59,28 +59,7 @@
59 </tal:not-editable>59 </tal:not-editable>
60</tal:affects-me-too>60</tal:affects-me-too>
6161
62<table62<tal:bugtask_table replace="structure context/@@+bugtasks-and-nominations-table" />
63 id="affected-software"
64 tal:attributes="class python: context.duplicateof and 'duplicate listing' or 'listing'"
65>
66 <thead>
67 <tr>
68 <th colspan="2">Affects</th>
69 <th>Status</th>
70 <th>Importance</th>
71 <th>Assigned to</th>
72 <th>Milestone</th>
73 </tr>
74 </thead>
75
76 <tbody>
77 <tal:bugtask-or-nomination
78 repeat="task_or_nom_view view/getBugTaskAndNominationViews">
79 <tal:block replace="structure task_or_nom_view" />
80 </tal:bugtask-or-nomination>
81 </tbody>
82
83</table>
8463
85<div class="actions"64<div class="actions"
86 tal:define="current_bugtask view/current_bugtask"65 tal:define="current_bugtask view/current_bugtask"
8766
=== added file 'lib/lp/bugs/templates/bugtasks-and-nominations-table.pt'
--- lib/lp/bugs/templates/bugtasks-and-nominations-table.pt 1970-01-01 00:00:00 +0000
+++ lib/lp/bugs/templates/bugtasks-and-nominations-table.pt 2011-11-04 14:06:00 +0000
@@ -0,0 +1,27 @@
1<tal:root
2 xmlns:tal="http://xml.zope.org/namespaces/tal"
3 xmlns:metal="http://xml.zope.org/namespaces/metal"
4 omit-tag="">
5 <table
6 id="affected-software"
7 tal:attributes="class python: context.duplicateof and 'duplicate listing' or 'listing'"
8 >
9 <thead>
10 <tr>
11 <th colspan="2">Affects</th>
12 <th>Status</th>
13 <th>Importance</th>
14 <th>Assigned to</th>
15 <th>Milestone</th>
16 </tr>
17 </thead>
18
19 <tbody>
20 <tal:bugtask-or-nomination
21 repeat="task_or_nom_view view/getBugTaskAndNominationViews">
22 <tal:block replace="structure task_or_nom_view" />
23 </tal:bugtask-or-nomination>
24 </tbody>
25
26 </table>
27</tal:root>