launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #05362
[Merge] lp:~wallyworld/launchpad/delete-bugtask-ui-ajax-878909 into lp:launchpad
Ian Booth has proposed merging lp:~wallyworld/launchpad/delete-bugtask-ui-ajax-878909 into lp:launchpad with lp:~wallyworld/launchpad/delete-bugtask-ui-878909 as a prerequisite.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~wallyworld/launchpad/delete-bugtask-ui-ajax-878909/+merge/80779
== Implementation ==
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.
--
https://code.launchpad.net/~wallyworld/launchpad/delete-bugtask-ui-ajax-878909/+merge/80779
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~wallyworld/launchpad/delete-bugtask-ui-ajax-878909 into lp:launchpad.
=== 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-02 04:10:43 +0000
@@ -40,9 +40,11 @@
var vocabulary = config.vocabulary_name;
var vocabulary_filters = config.vocabulary_filters;
var input_element = config.input_element;
- Y.on('domready', function(e) {
+ var show_widget_id = '${view/show_widget_id}';
+ var namespace = Y.namespace('lp.app.picker.connect');
+ namespace[show_widget_id] = function() {
// Sort out the Choose... link.
- var show_widget_node = Y.one('#${view/show_widget_id}');
+ var show_widget_node = Y.one('#'+show_widget_id);
show_widget_node.set('innerHTML', 'Choose…');
show_widget_node.addClass('js-action');
@@ -56,6 +58,9 @@
picker.show();
e.preventDefault();
});
+ };
+ Y.on('domready', function(e) {
+ namespace[show_widget_id]();
});
});
"/>
=== modified file 'lib/lp/bugs/browser/bugtask.py'
--- lib/lp/bugs/browser/bugtask.py 2011-11-02 04:10:42 +0000
+++ lib/lp/bugs/browser/bugtask.py 2011-11-02 04:10:43 +0000
@@ -119,6 +119,7 @@
isinstance as zope_isinstance,
removeSecurityProxy,
)
+from zope.traversing.browser import absoluteURL
from zope.traversing.interfaces import IPathAdapter
from canonical.config import config
@@ -678,11 +679,20 @@
cancel_url = canonical_url(self.context)
return cancel_url
+ @cachedproperty
+ def api_request(self):
+ return IWebServiceClientRequest(self.request)
+
def initialize(self):
"""Set up the needed widgets."""
bug = self.context.bug
cache = IJSONRequestCache(self.request)
cache.objects['bug'] = bug
+ subscribers_url_data = {
+ 'web_link': canonical_url(bug, rootsite='bugs'),
+ 'self_link': absoluteURL(bug, self.api_request),
+ }
+ cache.objects['subscribers_portlet_url_data'] = subscribers_url_data
cache.objects['total_comments_and_activity'] = (
self.total_comments + self.total_activity)
cache.objects['initial_comment_batch_offset'] = (
@@ -1525,7 +1535,8 @@
If yes, return True, otherwise return False.
"""
- return self.context.userCanEditImportance(self.user)
+ return (self.context.userCanEditImportance(self.user)
+ and not self.context.bugwatch)
def validate(self, data):
if self.show_sourcepackagename_widget and 'sourcepackagename' in data:
@@ -1771,13 +1782,42 @@
label = 'Remove bug task'
page_title = label
+ @property
+ def next_url(self):
+ """Return the next URL to call when this call completes."""
+ if not self.request.is_ajax:
+ return super(BugTaskDeletionView).next_url
+ return None
+
@action('Delete', name='delete_bugtask')
def delete_bugtask_action(self, action, data):
bugtask = self.context
+ bug = bugtask.bug
+ deleted_bugtask_url = canonical_url(self.context, rootsite='bugs')
message = ("This bug no longer affects %s."
% bugtask.target.bugtargetdisplayname)
bugtask.delete()
self.request.response.addNotification(message)
+ if self.request.is_ajax:
+ launchbag = getUtility(ILaunchBag)
+ launchbag.add(bug.default_bugtask)
+ # If we are deleting the current highlighted bugtask via ajax,
+ # we must force a redirect to the new default bugtask to ensure
+ # all URLs and other client cache content is correctly refreshed.
+ # We can't do the redirect here since the XHR caller won't see it
+ # so we return the URL to go to and let the caller do it.
+ if self._return_url == deleted_bugtask_url:
+ next_url = canonical_url(
+ bug.default_bugtask, rootsite='bugs')
+ self.request.response.setHeader('Content-type',
+ 'application/json')
+ return dumps(dict(bugtask_url=next_url))
+ # No redirect required so return the new bugtask table HTML.
+ view = getMultiAdapter(
+ (bug, self.request),
+ name='+bugtasks-and-nominations-table')
+ view.initialize()
+ return view.render()
class BugTaskListingView(LaunchpadView):
@@ -3662,6 +3702,10 @@
super(BugTaskTableRowView, self).__init__(context, request)
self.milestone_source = MilestoneVocabulary
+ @cachedproperty
+ def api_request(self):
+ return IWebServiceClientRequest(self.request)
+
def initialize(self):
super(BugTaskTableRowView, self).initialize()
link = canonical_url(self.context)
@@ -3698,8 +3742,17 @@
# We always look up all milestones, so there's no harm
# using len on the list here and avoid the COUNT query.
target_has_milestones=len(self._visible_milestones) > 0,
+ user_can_edit_status=not self.context.bugwatch,
)
+ if not self.many_bugtasks:
+ cache = IJSONRequestCache(self.request)
+ bugtask_data = cache.objects.get('bugtask_data', None)
+ if bugtask_data is None:
+ bugtask_data = dict()
+ cache.objects['bugtask_data'] = bugtask_data
+ bugtask_data[bugtask_id] = self.bugtask_config()
+
def canSeeTaskDetails(self):
"""Whether someone can see a task's status details.
@@ -3802,7 +3855,7 @@
items = vocabulary_to_choice_edit_items(
self._visible_milestones,
value_fn=lambda item: canonical_url(
- item, request=IWebServiceClientRequest(self.request)))
+ item, request=self.api_request))
items.append({
"name": "Remove milestone",
"disabled": False,
@@ -3822,7 +3875,8 @@
If yes, return True, otherwise return False.
"""
- return self.context.userCanEditImportance(self.user)
+ return (self.context.userCanEditImportance(self.user)
+ and not self.context.bugwatch)
@property
def user_can_edit_assignee(self):
@@ -3864,8 +3918,8 @@
else:
return ''
- def js_config(self):
- """Configuration for the JS widgets on the row, JSON-serialized."""
+ def bugtask_config(self):
+ """Configuration for the bugtask JS widgets on the row."""
assignee_vocabulary, assignee_vocabulary_filters = (
get_assignee_vocabulary_info(self.context))
# If we have no filters or just the ALL filter, then no filtering
@@ -3887,16 +3941,21 @@
not self.context.userCanSetAnyAssignee(user) and
(user is None or user.teams_participated_in.count() == 0))
cx = self.context
- return dumps(dict(
+ return dict(
row_id=self.data['row_id'],
+ form_row_id=self.data['form_row_id'],
bugtask_path='/'.join([''] + self.data['link'].split('/')[3:]),
prefix=get_prefix(cx),
+ targetname=cx.target.bugtargetdisplayname,
+ bug_title=cx.bug.title,
assignee_value=cx.assignee and cx.assignee.name,
assignee_is_team=cx.assignee and cx.assignee.is_team,
assignee_vocabulary=assignee_vocabulary,
assignee_vocabulary_filters=filter_details,
hide_assignee_team_selection=hide_assignee_team_selection,
user_can_unassign=cx.userCanUnassign(user),
+ user_can_delete=self.user_can_delete_bugtask,
+ delete_link=self.data['delete_link'],
target_is_product=IProduct.providedBy(cx.target),
status_widget_items=self.status_widget_items,
status_value=cx.status.title,
@@ -3906,14 +3965,13 @@
milestone_value=(
canonical_url(
cx.milestone,
- request=IWebServiceClientRequest(self.request))
+ request=self.api_request)
if cx.milestone else None),
user_can_edit_assignee=self.user_can_edit_assignee,
user_can_edit_milestone=self.user_can_edit_milestone,
user_can_edit_status=not cx.bugwatch,
- user_can_edit_importance=(
- self.user_can_edit_importance and not cx.bugwatch)
- ))
+ user_can_edit_importance=self.user_can_edit_importance,
+ )
class BugsBugTaskSearchListingView(BugTaskSearchListingView):
=== modified file 'lib/lp/bugs/browser/configure.zcml'
--- lib/lp/bugs/browser/configure.zcml 2011-11-02 04:10:42 +0000
+++ lib/lp/bugs/browser/configure.zcml 2011-11-02 04:10:43 +0000
@@ -1029,6 +1029,12 @@
for="lp.bugs.interfaces.bug.IBug"
class="lp.bugs.browser.bugtask.BugTasksAndNominationsView"
permission="launchpad.View"
+ name="+bugtasks-and-nominations-portal"
+ template="../templates/bugtasks-and-nominations-portal.pt"/>
+ <browser:page
+ for="lp.bugs.interfaces.bug.IBug"
+ class="lp.bugs.browser.bugtask.BugTasksAndNominationsView"
+ permission="launchpad.View"
name="+bugtasks-and-nominations-table"
template="../templates/bugtasks-and-nominations-table.pt"/>
<browser:page
=== 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-02 04:10:43 +0000
@@ -432,9 +432,20 @@
BugTasks and Nominations Table
------------------------------
-A table is rendered at the top of the bug page which shows both bugtasks
-and nominations. This table is rendered with the
-+bugtasks-and-nomination-table view.
+Content is rendered at the top of the bug page which shows both bugtasks
+and nominations and various links like "Does this bug affect you" and
+"Also Affects Project" etc. This content is rendered with the
++bugtasks-and-nominations-portal view.
+
+ >>> request = LaunchpadTestRequest()
+
+ >>> bugtasks_and_nominations_view = getMultiAdapter(
+ ... (bug_one_bugtask.bug, request),
+ ... name="+bugtasks-and-nominations-portal")
+ >>> bugtasks_and_nominations_view.initialize()
+
+The bugtasks and nominations table itself is rendered with the
++bugtasks-and-nominations-table view.
>>> request = LaunchpadTestRequest()
=== modified file 'lib/lp/bugs/browser/tests/test_bugtask.py'
--- lib/lp/bugs/browser/tests/test_bugtask.py 2011-11-02 04:10:42 +0000
+++ lib/lp/bugs/browser/tests/test_bugtask.py 2011-11-02 04:10:43 +0000
@@ -6,6 +6,7 @@
from contextlib import contextmanager
from datetime import datetime
import re
+import simplejson
import urllib
from lazr.lifecycle.event import ObjectModifiedEvent
@@ -569,7 +570,7 @@
request = LaunchpadTestRequest()
foo_bugtasks_and_nominations_view = getMultiAdapter(
- (foo_bug, request), name="+bugtasks-and-nominations-table")
+ (foo_bug, request), name="+bugtasks-and-nominations-portal")
foo_bugtasks_and_nominations_view.initialize()
task_and_nomination_views = (
@@ -593,7 +594,7 @@
request = LaunchpadTestRequest()
foo_bugtasks_and_nominations_view = getMultiAdapter(
- (foo_bug, request), name="+bugtasks-and-nominations-table")
+ (foo_bug, request), name="+bugtasks-and-nominations-portal")
foo_bugtasks_and_nominations_view.initialize()
task_and_nomination_views = (
@@ -689,6 +690,37 @@
'bugtask-delete-task%d' % bug.default_bugtask.id)
self.assertIsNone(delete_icon)
+ def test_client_cache_contents(self):
+ """ Test that the client cache contains the expected data.
+
+ The cache data is used by the Javascript to enable the delete
+ links to work as expected.
+ """
+ bug = self.factory.makeBug()
+ bugtask_owner = self.factory.makePerson()
+ bugtask = self.factory.makeBugTask(bug=bug, owner=bugtask_owner)
+ with FeatureFixture(DELETE_BUGTASK_ENABLED):
+ login_person(bugtask.owner)
+ getUtility(ILaunchBag).add(bug.default_bugtask)
+ view = create_initialized_view(
+ bug, name='+bugtasks-and-nominations-table',
+ principal=bugtask.owner)
+ view.render()
+ cache = IJSONRequestCache(view.request)
+ all_bugtask_data = cache.objects['bugtask_data']
+
+ def check_bugtask_data(bugtask, can_delete):
+ self.assertIn(bugtask.id, all_bugtask_data)
+ bugtask_data = all_bugtask_data[bugtask.id]
+ self.assertEqual(
+ 'task%d' % bugtask.id, bugtask_data['form_row_id'])
+ self.assertEqual(
+ 'tasksummary%d' % bugtask.id, bugtask_data['row_id'])
+ self.assertEqual(can_delete, bugtask_data['user_can_delete'])
+
+ check_bugtask_data(bug.default_bugtask, False)
+ check_bugtask_data(bugtask, True)
+
class TestBugTaskDeleteView(TestCaseWithFactory):
"""Test the bug task delete form."""
@@ -734,6 +766,82 @@
expected = 'This bug no longer affects %s.' % target_name
self.assertEqual(expected, notifications[0].message)
+ def test_ajax_delete_current_bugtask(self):
+ # Test that deleting the current bugtask returns a JSON dict
+ # containing the URL of the bug's default task to redirect to.
+ bug = self.factory.makeBug()
+ bugtask = self.factory.makeBugTask(bug=bug)
+ target_name = bugtask.bugtargetdisplayname
+ bugtask_url = canonical_url(bugtask, rootsite='bugs')
+ with FeatureFixture(DELETE_BUGTASK_ENABLED):
+ login_person(bugtask.owner)
+ # Set up the request so that we correctly simulate an XHR call
+ # from the URL of the bugtask we are deleting.
+ server_url = canonical_url(
+ getUtility(ILaunchpadRoot), rootsite='bugs')
+ extra = {
+ 'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest',
+ 'HTTP_REFERER': bugtask_url,
+ }
+ form = {
+ 'field.actions.delete_bugtask': 'Delete'
+ }
+ view = create_initialized_view(
+ bugtask, name='+delete', server_url=server_url, form=form,
+ principal=bugtask.owner, **extra)
+ result_data = simplejson.loads(view.render())
+ self.assertEqual([bug.default_bugtask], bug.bugtasks)
+ notifications = simplejson.loads(
+ view.request.response.getHeader('X-Lazr-Notifications'))
+ self.assertEqual(1, len(notifications))
+ expected = 'This bug no longer affects %s.' % target_name
+ self.assertEqual(expected, notifications[0][1])
+ self.assertEqual(
+ view.request.response.getHeader('content-type'),
+ 'application/json')
+ expected_url = canonical_url(bug.default_bugtask, rootsite='bugs')
+ self.assertEqual(dict(bugtask_url=expected_url), result_data)
+
+ def test_ajax_delete_non_current_bugtask(self):
+ # Test that deleting the non-current bugtask returns the new bugtasks
+ # table as HTML.
+ bug = self.factory.makeBug()
+ bugtask = self.factory.makeBugTask(bug=bug)
+ target_name = bugtask.bugtargetdisplayname
+ default_bugtask_url = canonical_url(
+ bug.default_bugtask, rootsite='bugs')
+ with FeatureFixture(DELETE_BUGTASK_ENABLED):
+ login_person(bugtask.owner)
+ # Set up the request so that we correctly simulate an XHR call
+ # from the URL of the default bugtask, not the one we are
+ # deleting.
+ server_url = canonical_url(
+ getUtility(ILaunchpadRoot), rootsite='bugs')
+ extra = {
+ 'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest',
+ 'HTTP_REFERER': default_bugtask_url,
+ }
+ form = {
+ 'field.actions.delete_bugtask': 'Delete'
+ }
+ view = create_initialized_view(
+ bugtask, name='+delete', server_url=server_url, form=form,
+ principal=bugtask.owner, **extra)
+ result_html = view.render()
+ self.assertEqual([bug.default_bugtask], bug.bugtasks)
+ notifications = view.request.response.notifications
+ self.assertEqual(1, len(notifications))
+ expected = 'This bug no longer affects %s.' % target_name
+ self.assertEqual(expected, notifications[0].message)
+ self.assertEqual(
+ view.request.response.getHeader('content-type'), 'text/html')
+ table = find_tag_by_id(result_html, 'affected-software')
+ self.assertIsNotNone(table)
+ [row] = table.tbody.findAll('tr', {'class': 'highlight'})
+ target_link = row.find('a', {'class': 'sprite product'})
+ self.assertIn(
+ bug.default_bugtask.bugtargetdisplayname, target_link)
+
class TestBugTasksAndNominationsViewAlsoAffects(TestCaseWithFactory):
""" Tests the boolean methods on the view used to indicate whether the
@@ -752,7 +860,7 @@
def _createView(self, bug):
request = LaunchpadTestRequest()
bugtasks_and_nominations_view = getMultiAdapter(
- (bug, request), name="+bugtasks-and-nominations-table")
+ (bug, request), name="+bugtasks-and-nominations-portal")
return bugtasks_and_nominations_view
def test_project_bug_cannot_affect_something_else(self):
=== 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-02 04:10:43 +0000
@@ -11,6 +11,9 @@
var namespace = Y.namespace('lp.bugs.bugtask_index');
+// Override for testing
+namespace.ANIM_DURATION = 1;
+
// lazr.FormOverlay objects.
var duplicate_form_overlay;
var privacy_form_overlay;
@@ -246,7 +249,10 @@
dupe_span.one('a').set('href', update_dupe_url);
hide_comment_on_duplicate_warning();
}
- Y.lp.anim.green_flash({node: dupe_span}).run();
+ Y.lp.anim.green_flash({
+ node: dupe_span,
+ duration: namespace.ANIM_DURATION
+ }).run();
// ensure the new link is hooked up correctly:
dupe_span.one('a').on(
'click', function(e){
@@ -355,7 +361,10 @@
privacy_link.setStyle('display', 'inline');
};
error_handler.showError = function (error_msg) {
- Y.lp.anim.red_flash({node: privacy_div}).run();
+ Y.lp.anim.red_flash({
+ node: privacy_div,
+ duration: namespace.ANIM_DURATION
+ }).run();
privacy_form_overlay.showError(error_msg);
privacy_form_overlay.show();
};
@@ -438,7 +447,10 @@
}
Y.lp.client.display_notifications(
response.getResponseHeader('X-Lazr-Notifications'));
- Y.lp.anim.green_flash({node: privacy_div}).run();
+ Y.lp.anim.green_flash({
+ node: privacy_div,
+ duration: namespace.ANIM_DURATION
+ }).run();
},
failure: error_handler.getFailureHandler()
}
@@ -578,9 +590,15 @@
var bug_branch_container = Y.one('#bug-branches-container');
bug_branch_container.appendChild(bug_branch_list);
- anim = Y.lp.anim.green_flash({node: bug_branch_list});
+ anim = Y.lp.anim.green_flash({
+ node: bug_branch_list,
+ duration: namespace.ANIM_DURATION
+ });
} else {
- anim = Y.lp.anim.green_flash({node: bug_branch_node});
+ anim = Y.lp.anim.green_flash({
+ node: bug_branch_node,
+ duration: namespace.ANIM_DURATION
+ });
}
var existing_bug_branch_node = bug_branch_list.one(
@@ -591,7 +609,10 @@
bug_branch_list.appendChild(bug_branch_node);
} else {
// If the bug branch exists already, flash it.
- anim = Y.lp.anim.green_flash({node: existing_bug_branch_node});
+ anim = Y.lp.anim.green_flash({
+ node: existing_bug_branch_node,
+ duration: namespace.ANIM_DURATION
+ });
}
anim.run();
// Fire of the generic branch linked event.
@@ -624,6 +645,198 @@
};
/**
+ * Set up the bug task table.
+ *
+ * Called once on load, to initialize the page, and also when the contents of
+ * the bug task table is replaced after an XHR call.
+ *
+ * @method setup_bugtask_table
+ */
+namespace.setup_bugtask_table = function() {
+ var bugtask_data = LP.cache.bugtask_data;
+ if (!Y.Lang.isValue(bugtask_data)) {
+ return;
+ }
+ var picker_connect = Y.namespace('lp.app.picker.connect');
+ for (var id in bugtask_data) {
+ if( bugtask_data.hasOwnProperty(id) ) {
+ var conf = bugtask_data[id];
+ // We need to wire the target and assignee pickers in the
+ // expandable bugtask edit form. This setup_bugtask_table() method
+ // is called when the page loads as well as after replacing the
+ // table. On page load, the pickers are wired by javascript
+ // embedded in the picker tales so we need to ensure we handle
+ // this case.
+ var tr = Y.one('#' + conf.form_row_id);
+ if ( tr === null ) {
+ //The row has been deleted.
+ continue;
+ }
+ tr.all('a').each(function(link) {
+ // The link may already have been processed.
+ if( link.hasClass('js-action') ) {
+ return;
+ }
+ var func_name = link.get('id');
+ var connect_func = picker_connect[func_name];
+ if( Y.Lang.isFunction(connect_func)) {
+ connect_func();
+ }
+ });
+ // Now wire up the javascript widgets in the table row.
+ namespace.setup_bugtask_row(conf);
+ }
+ }
+};
+
+/**
+ * Show a spinner next to the delete icon.
+ *
+ * @method _showDeleteSpinner
+ */
+namespace._showDeleteSpinner = function(delete_link) {
+ var spinner_node = Y.Node.create(
+ '<img class="spinner" src="/@@/spinner" alt="Deleting..." />');
+ delete_link.insertBefore(spinner_node, delete_link);
+ delete_link.addClass('unseen');
+};
+
+/**
+ * Hide the delete spinner.
+ *
+ * @method _hideDeleteSpinner
+ */
+namespace._hideDeleteSpinner = function(delete_link) {
+ delete_link.removeClass('unseen');
+ var spinner = delete_link.get('parentNode').one('.spinner');
+ if (spinner !== null) {
+ spinner.remove();
+ }
+};
+
+/**
+ * Replace the currect bugtask table with a new one, ensuring all Javascript
+ * widgets are correctly wired up.
+ *
+ * @method _render_bugtask_table
+ */
+namespace._render_bugtask_table = function(new_table) {
+ var bugtask_table = Y.one('#affected-software');
+ bugtask_table.replace(new_table);
+ namespace.setup_bugtask_table();
+};
+
+/**
+ * Prompt the user to confirm the deletion of the selected bugtask.
+ * widgets are correctly wired up.
+ *
+ * @method _confirm_bugtask_delete
+ */
+namespace._confirm_bugtask_delete = function(delete_link, conf) {
+ var delete_text = [
+ '<p class="large-warning" style="padding:2px 2px 0 36px;">',
+ 'You are about to mark bug "',
+ conf.bug_title,
+ '"<br>as no longer affecting ',
+ conf.targetname,
+ '.<br><br><strong>Please confirm you really want to do this.',
+ '</strong></p>'
+ ].join('');
+ var co = new Y.lp.app.confirmationoverlay.ConfirmationOverlay({
+ submit_fn: function() {
+ namespace.delete_bugtask(delete_link, conf);
+ },
+ form_content: delete_text,
+ headerContent: '<h2>Confirm bugtask deletion</h2>'
+ });
+ co.show();
+};
+
+/**
+ * Redirect to a new URL. We need to break this out to allow testing.
+ *
+ * @method _redirect
+ */
+namespace._redirect = function(url) {
+ window.location.replace(url);
+};
+
+/**
+ * Process the result of the XHR request to delete a bugtask.
+ *
+ * @method _process_bugtask_delete_response
+ */
+namespace._process_bugtask_delete_response = function(response, row_id) {
+ // If the result is json, then we need to perform a redirect to a new
+ // bugtask URL. This happens when the current bugtask is deleted and we
+ // need to ensure all link URLS are correctly reset.
+ var content_type = response.getResponseHeader('Content-type');
+ if( content_type === 'application/json' ) {
+ Y.lp.client.display_notifications(
+ response.getResponseHeader('X-Lazr-Notifications'));
+ var redirect = Y.JSON.parse(response.responseText);
+ Y.lp.anim.red_flash({
+ node: '#' + row_id,
+ duration: namespace.ANIM_DURATION
+ }).run();
+ namespace._redirect(redirect.bugtask_url);
+ return;
+ }
+ // We have received HTML, so we replace the current bugtask table with a
+ // new one.
+ var on = {
+ end: function() {
+ namespace._render_bugtask_table(response.responseText);
+ Y.lp.client.display_notifications(
+ response.getResponseHeader('X-Lazr-Notifications'));
+ }
+ };
+ Y.lp.anim.red_flash({
+ node: '#' + row_id,
+ duration: namespace.ANIM_DURATION,
+ on: on
+ }).run();
+};
+
+/**
+ * Delete the bugtask defined by the delete_link using an XHR call.
+ *
+ * @method delete_bugtask
+ */
+namespace.delete_bugtask = function (delete_link, conf) {
+ Y.lp.client.remove_notifications();
+ var error_handler = new Y.lp.client.ErrorHandler();
+ error_handler.showError = Y.bind(function (error_msg) {
+ namespace._hideDeleteSpinner(delete_link);
+ Y.lp.app.errors.display_error(undefined, error_msg);
+ }, this);
+
+ var submit_url = delete_link.get('href');
+ var qs = Y.lp.client.append_qs(
+ '', 'field.actions.delete_bugtask', 'Delete');
+ var y_config = {
+ method: "POST",
+ headers: {'Accept': 'application/json; application/xhtml'},
+ on: {
+ start:
+ function() {
+ namespace._showDeleteSpinner(delete_link);
+ },
+ failure:
+ error_handler.getFailureHandler(),
+ success:
+ function(id, response) {
+ namespace._process_bugtask_delete_response(
+ response, conf.row_id);
+ }
+ },
+ data: qs
+ };
+ var io_provider = Y.lp.client.get_configured_io_provider(conf);
+ io_provider.io(submit_url, y_config);
+};
+
+/**
* Set up a bug task table row.
*
* Called once per row, on load, to initialize the page.
@@ -641,6 +854,15 @@
var importance_content = tr.one('.importance-content');
var assignee_content = Y.one('#assignee-picker-' + conf.row_id);
var milestone_content = tr.one('.milestone-content');
+ var delete_link = tr.one('.bugtask-delete');
+
+ if (Y.Lang.isValue(LP.links.me) && Y.Lang.isValue(delete_link)
+ && conf.user_can_delete) {
+ delete_link.on('click', (function (e) {
+ e.preventDefault();
+ namespace._confirm_bugtask_delete(delete_link, conf);
+ }));
+ }
if (status_content === null) {
// Not all table rows have status widgets. If this is one of those
@@ -894,6 +1116,19 @@
}
assignee_picker.render();
}
+
+ // Set-up the expander on the bug task summary row.
+ var icon_node = Y.one('tr#' + conf.row_id + ' a.bugtask-expander');
+ var row_node = Y.one('tr#' + conf.form_row_id);
+ if (Y.Lang.isValue(row_node)) {
+ // When no row is present, this is bug task on a project with
+ // multiple per-series tasks, so we do not need to set
+ // the expander for the descriptive parent project task.
+ var content_node = row_node.one('td form');
+ var expander = new Y.lp.app.widgets.expander.Expander(
+ icon_node, row_node, { animate_node: content_node });
+ expander.setUp();
+ }
};
/**
@@ -1122,7 +1357,8 @@
comments_container.appendChild(new_comments_node);
if (Y.Lang.isValue(Y.lp.anim)) {
var success_anim = Y.lp.anim.green_flash(
- {node: new_comments_node});
+ {node: new_comments_node,
+ duration: namespace.ANIM_DURATION});
success_anim.run();
}
batch_url_div = Y.one('#next-batch-url');
@@ -1209,6 +1445,6 @@
"lazr.formoverlay", "lp.anim", "lazr.base",
"lazr.overlay", "lazr.choiceedit", "lp.app.picker",
"lp.bugs.bugtask_index.portlets.subscription",
- "lp.client", "escape",
+ "lp.app.widgets.expander", "lp.client", "escape",
"lp.client.plugins", "lp.app.errors",
- "lp.app.privacy"]});
+ "lp.app.privacy", "lp.app.confirmationoverlay"]});
=== 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-02 04:10:43 +0000
@@ -41,9 +41,14 @@
* a relative URI to load subscribers' details from.
*/
function createBugSubscribersLoader(config) {
+ var url_data = LP.cache.subscribers_portlet_url_data;
+ if (!Y.Lang.isValue(url_data)) {
+ url_data = { self_link: LP.cache.context.bug_link,
+ web_link: LP.cache.context.web_link };
+ }
config.subscriber_levels = subscriber_levels;
config.subscriber_level_order = subscriber_level_order;
- config.context = config.bug;
+ config.context = url_data;
config.subscribe_someone_else_level = 'Discussion';
config.default_subscriber_level = 'Maybe';
var module = Y.lp.app.subscribers.subscribers_list;
=== 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-02 04:10:43 +0000
@@ -0,0 +1,55 @@
+<html>
+ <head>
+ <title>Bug task deletion</title>
+
+ <!-- YUI and test setup -->
+ <script type="text/javascript"
+ src="../../../../canonical/launchpad/icing/yui/yui/yui.js">
+ </script>
+ <link rel="stylesheet" href="../../../app/javascript/testing/test.css" />
+ <script type="text/javascript"
+ src="../../../app/javascript/testing/testrunner.js"></script>
+
+ <script type="text/javascript" src="../../../app/javascript/client.js"></script>
+ <script type="text/javascript" src="../../../app/javascript/errors.js"></script>
+ <script type="text/javascript" src="../../../app/javascript/lp.js"></script>
+
+ <!-- Other dependencies -->
+ <script type="text/javascript" src="../../../app/javascript/testing/mockio.js"></script>
+ <script type="text/javascript"
+ src="../../../contrib/javascript/mustache.js"></script>
+ <script type="text/javascript" src="../../../app/javascript/activator/activator.js"></script>
+ <script type="text/javascript" src="../../../app/javascript/anim/anim.js"></script>
+ <script type="text/javascript" src="../../../app/javascript/confirmationoverlay/confirmationoverlay.js"></script>
+ <script type="text/javascript" src="../../../app/javascript/choiceedit/choiceedit.js"></script>
+ <script type="text/javascript" src="../../../app/javascript/effects/effects.js"></script>
+ <script type="text/javascript" src="../../../app/javascript/expander.js"></script>
+ <script type="text/javascript" src="../../../app/javascript/extras/extras.js"></script>
+ <script type="text/javascript" src="../../../app/javascript/formoverlay/formoverlay.js"></script>
+ <script type="text/javascript" src="../../../app/javascript/inlineedit/editor.js"></script>
+ <script type="text/javascript" src="../../../app/javascript/lazr/lazr.js"></script>
+ <script type="text/javascript" src="../../../app/javascript/overlay/overlay.js"></script>
+ <script type="text/javascript" src="../../../app/javascript/picker/picker.js"></script>
+ <script type="text/javascript" src="../../../app/javascript/picker/picker_patcher.js"></script>
+ <script type="text/javascript" src="../../../app/javascript/picker/person_picker.js"></script>
+ <script type="text/javascript" src="../../../app/javascript/privacy.js"></script>
+ <script type="text/javascript" src="../bug_subscription_portlet.js"></script>
+
+ <!-- The module under test -->
+ <script type="text/javascript"
+ src="../bugtask_index.js"></script>
+
+ <!-- The test suite -->
+ <script type="text/javascript"
+ src="test_bugtask_delete.js"></script>
+
+ <!-- Pretty up the sample html -->
+ <style type="text/css">
+ div#sample {margin:15px; width:200px; border:1px solid #999; padding:10px;}
+ </style>
+ </head>
+ <body class="yui3-skin-sam">
+ <div id="fixture"></div>
+ <div id="request-notifications"></div>
+ </body>
+</html>
=== 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-02 04:10:43 +0000
@@ -0,0 +1,241 @@
+YUI().use('lp.testing.runner', 'lp.testing.mockio', 'base', 'test', 'console',
+ 'node', 'node-event-simulate', 'lp.bugs.bugtask_index',
+ function(Y) {
+
+var suite = new Y.Test.Suite("Bugtask deletion Tests");
+var module = Y.lp.bugs.bugtask_index;
+
+
+suite.add(new Y.Test.Case({
+ name: 'Bugtask delete',
+
+ setUp: function() {
+ module.ANIM_DURATION = 0;
+ this.link_conf = {
+ row_id: 'tasksummary49',
+ form_row_id: 'tasksummary49',
+ user_can_delete: true
+ };
+ window.LP = {
+ links: {me : "/~user"},
+ cache: {
+ bugtask_data: {49: this.link_conf}
+ }
+ };
+ this.fixture = Y.one('#fixture');
+ var bugtasks_table = Y.Node.create([
+ '<table id="affected-software">',
+ ' <tbody>',
+ ' <tr id="tasksummary49">',
+ ' <td>',
+ ' <div>',
+ ' <a class="sprite product" href="#">Product</a>',
+ ' <button class="lazr-btn yui3-activator-act">',
+ ' Edit',
+ ' </button>',
+ ' <a id="bugtask-delete-task49" href="http://foo" ',
+ ' class="sprite remove bugtask-delete"></a>',
+ ' </div>',
+ ' </td>',
+ ' <td><table><tr id="task49"><td>',
+ ' <a id="show-widget-product" class="sprite product" ',
+ ' href="#">Product</a></td>',
+ ' </td></tr></table></td>',
+ ' </tr></tbody></table>'].join(''));
+
+ this.fixture.appendChild(bugtasks_table);
+ this.delete_link = bugtasks_table.one('#bugtask-delete-task49');
+ },
+
+ tearDown: function() {
+ if (this.fixture !== null) {
+ this.fixture.empty();
+ }
+ Y.one('#request-notifications').empty();
+ delete this.fixture;
+ delete window.LP;
+ },
+
+ test_show_spinner: function() {
+ // Test the delete progress spinner is shown.
+ module._showDeleteSpinner(this.delete_link);
+ Y.Assert.isNotNull(this.fixture.one('.spinner'));
+ Y.Assert.isTrue(this.delete_link.hasClass('unseen'));
+ },
+
+ test_hide_spinner: function() {
+ // Test the delete progress spinner is hidden.
+ module._showDeleteSpinner(this.delete_link);
+ module._hideDeleteSpinner(this.delete_link);
+ Y.Assert.isNull(this.fixture.one('.spinner'));
+ Y.Assert.isFalse(this.delete_link.hasClass('unseen'));
+ },
+
+ _test_delete_confirmation: function(click_ok) {
+ // Test the delete confirmation dialog when delete is clicked.
+ var orig_delete_bugtask = module.delete_bugtask;
+
+ var delete_called = false;
+ var self = this;
+ module.delete_bugtask = function(delete_link, conf) {
+ Y.Assert.areEqual(self.delete_link, delete_link);
+ Y.Assert.areEqual(self.link_conf, conf);
+ delete_called = true;
+ };
+ module.setup_bugtask_table();
+ this.delete_link.simulate('click');
+ var co = Y.one('.yui3-overlay.yui3-lp-app-confirmationoverlay');
+ var actions = co.one('.yui3-lazr-formoverlay-actions');
+ var btn_style;
+ if (click_ok) {
+ btn_style = '.ok-btn';
+ } else {
+ btn_style = '.cancel-btn';
+ }
+ var button = actions.one(btn_style);
+ button.simulate('click');
+ Y.Assert.areEqual(click_ok, delete_called);
+ Y.Assert.isTrue(
+ co.hasClass('yui3-lp-app-confirmationoverlay-hidden'));
+ module.delete_bugtask = orig_delete_bugtask;
+ },
+
+ test_delete_confirmation_ok: function() {
+ // Test the delete confirmation dialog Ok functionality.
+ this._test_delete_confirmation(true);
+ },
+
+ test_delete_confirmation_cancel: function() {
+ // Test the delete confirmation dialog Cancel functionality.
+ this._test_delete_confirmation(false);
+ },
+
+ test_setup_bugtask_table: function() {
+ // Test that the bugtask table is wired up, the pickers and the
+ // delete links etc.
+ var namespace = Y.namespace('lp.app.picker.connect');
+ var connect_picker_called = false;
+ namespace['show-widget-product'] = function() {
+ connect_picker_called = true;
+ };
+ var orig_confirm_bugtask_delete = module._confirm_bugtask_delete;
+ var self = this;
+ var confirm_delete_called = false;
+ module._confirm_bugtask_delete = function(delete_link, conf) {
+ Y.Assert.areEqual(self.delete_link, delete_link);
+ Y.Assert.areEqual(self.link_conf, conf);
+ confirm_delete_called = true;
+ };
+ module.setup_bugtask_table();
+ this.delete_link.simulate('click');
+ Y.Assert.isTrue(connect_picker_called);
+ Y.Assert.isTrue(confirm_delete_called);
+ module._confirm_bugtask_delete = orig_confirm_bugtask_delete;
+ },
+
+ test_render_bugtask_table: function() {
+ // Test that a new bug task table is rendered and setup.
+ var orig_setup_bugtask_table = module.setup_bugtask_table;
+ var setup_called = false;
+ module.setup_bugtask_table = function() {
+ setup_called = true;
+ };
+ var test_table =
+ '<table id="affected-software">'+
+ '<tr><td>foo</td></tr></table>';
+ module._render_bugtask_table(test_table);
+ Y.Assert.isTrue(setup_called);
+ Y.Assert.areEqual(
+ '<tbody><tr><td>foo</td></tr></tbody>',
+ this.fixture.one('table#affected-software').getContent());
+ module.setup_bugtask_table = orig_setup_bugtask_table;
+ },
+
+ test_process_bugtask_delete_redirect_response: function() {
+ // Test the processing of a XHR delete result which is to
+ // redirect the browser to a new URL.
+ var orig_redirect = module._redirect;
+ var redirect_called = false;
+ module._redirect = function(url) {
+ Y.Assert.areEqual('http://foo', url);
+ redirect_called = true;
+ };
+ var response = new Y.lp.testing.mockio.MockHttpResponse({
+ responseText: '{"bugtask_url": "http://foo"}',
+ responseHeaders: {'Content-type': 'application/json'}});
+ module._process_bugtask_delete_response(
+ response, this.link_conf.row_id);
+ this.wait(function() {
+ // Wait for the animation to complete.
+ Y.Assert.isTrue(redirect_called);
+ }, 10);
+ module._redirect = orig_redirect;
+ },
+
+ test_process_bugtask_delete_new_table_response: function() {
+ // Test the processing of a XHR delete result which is to
+ // replace the current bugtasks table.
+ var orig_render_bugtask_table = module._render_bugtask_table;
+ var render_table_called = false;
+ module._render_bugtask_table = function(new_table) {
+ Y.Assert.areEqual('<table>Foo</table>', new_table);
+ render_table_called = true;
+ };
+ var notifications = '[ [20, "Delete Success"] ]';
+ var response = new Y.lp.testing.mockio.MockHttpResponse({
+ responseText: '<table>Foo</table>',
+ responseHeaders: {
+ 'Content-type': 'text/html',
+ 'X-Lazr-Notifications': notifications}});
+ module._process_bugtask_delete_response(
+ response, this.link_conf.row_id);
+ this.wait(function() {
+ // Wait for the animation to complete.
+ Y.Assert.isTrue(render_table_called);
+ var node = Y.one('div#request-notifications ' +
+ 'div.informational.message');
+ Y.Assert.areEqual('Delete Success', node.getContent());
+ }, 10);
+ module._render_bugtask_table = orig_render_bugtask_table;
+ },
+
+ test_delete_bugtask: function() {
+ // Test that when delete_bugtask is called, the expected XHR call
+ // is made.
+ var orig_delete_repsonse =
+ module._process_bugtask_delete_response;
+
+ var delete_response_called = false;
+ var self = this;
+ module._process_bugtask_delete_response = function(response, id) {
+ Y.Assert.areEqual('<p>Foo</p>', response.responseText);
+ Y.Assert.areEqual(self.link_conf.row_id, id);
+ delete_response_called = true;
+ };
+
+ var mockio = new Y.lp.testing.mockio.MockIo();
+ var conf = Y.merge(this.link_conf, {io_provider: mockio});
+ module.delete_bugtask(this.delete_link, conf);
+ mockio.success({
+ responseText: '<p>Foo</p>',
+ responseHeaders: {'Content-Type': 'text/html'}});
+ // Check the parameters passed to the io call.
+ Y.Assert.areEqual(
+ this.delete_link.get('href'),
+ mockio.last_request.url);
+ Y.Assert.areEqual(
+ 'POST', mockio.last_request.config.method);
+ Y.Assert.areEqual(
+ 'application/json; application/xhtml',
+ mockio.last_request.config.headers.Accept);
+ Y.Assert.areEqual(
+ 'field.actions.delete_bugtask=Delete',
+ mockio.last_request.config.data);
+ Y.Assert.isTrue(delete_response_called);
+
+ module._process_bugtask_delete_response = orig_delete_repsonse;
+ }
+}));
+
+Y.lp.testing.Runner.run(suite);
+});
=== 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-02 04:10:43 +0000
@@ -12,19 +12,26 @@
setUp: function() {
this.root = Y.Node.create('<div />');
Y.one('body').appendChild(this.root);
+ window.LP = {
+ cache: {
+ context: {
+ bug_link: '/bug/1',
+ web_link: '/base'
+ }
+ }
+ };
},
tearDown: function() {
this.root.remove();
+ delete window.LP;
},
setUpLoader: function() {
this.root.appendChild(
Y.Node.create('<div />').addClass('container'));
- var bug = { web_link: '/base', self_link: '/bug/1'};
return new module.createBugSubscribersLoader({
container_box: '.container',
- bug: bug,
subscribers_details_view: '/+bug-portlet-subscribers-details'});
},
=== 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-02 04:10:43 +0000
@@ -20,13 +20,11 @@
Y.lp.code.branchmergeproposal.diff.connect_diff_links();
}, window);
Y.on('domready', function() {
- var bug = { self_link: LP.cache.context.bug_link,
- web_link: LP.cache.context.web_link };
+ Y.lp.bugs.bugtask_index.setup_bugtask_table();
LP.cache.comment_context = LP.cache.bug;
Y.lp.comments.hide.setup_hide_controls();
var sl = new Y.lp.bugs.subscribers.createBugSubscribersLoader({
container_box: '#other-bug-subscribers',
- bug: bug,
subscribers_details_view:
'/+bug-portlet-subscribers-details',
subscribe_someone_else_link: '.menu-link-addsubscriber'
@@ -138,8 +136,7 @@
<tal:heat replace="structure view/bug_heat_html" />
</div>
- <div tal:replace="structure context/bug/@@+bugtasks-and-nominations-table" />
-
+ <div tal:replace="structure context/bug/@@+bugtasks-and-nominations-portal" />
<div id="maincontentsub">
<div><!-- id="nonportlets"> -->
<div class="top-portlet">
=== 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-02 04:10:42 +0000
+++ lib/lp/bugs/templates/bugtask-tasks-and-nominations-table-row.pt 2011-11-02 04:10:43 +0000
@@ -58,7 +58,8 @@
</tal:conjoined-task>
<tal:not-conjoined-task condition="not:data/is_conjoined_slave">
- <td style="width: 20%; vertical-align: middle">
+ <td tal:condition="data/user_can_edit_status"
+ style="width: 20%; vertical-align: middle">
<div class="status-content"
style="width: 100%; float: left"
tal:define="status context/status">
@@ -170,30 +171,6 @@
</tal:not-conjoined-task>
</tr>
- <script type="text/javascript"
- class="bugtasks-table-row-init-script"
- tal:condition="not:view/many_bugtasks"
- tal:content="string:
- LPS.use('event', 'lp.bugs.bugtask_index', 'lp.app.widgets.expander',
- function(Y) {
- Y.on('domready', function() {
- Y.lp.bugs.bugtask_index.setup_bugtask_row(${view/js_config});
-
- // Set-up the expander on the bug task summary row.
- var icon_node = Y.one('tr#${data/row_id} a.bugtask-expander');
- var row_node = Y.one('tr#${data/form_row_id}');
- if (Y.Lang.isValue(row_node)) {
- // When no row is present, this is bug task on a project with
- // multiple per-series tasks, so we do not need to set
- // the expander for the descriptive parent project task.
- var content_node = row_node.one('td form');
- var expander = new Y.lp.app.widgets.expander.Expander(
- icon_node, row_node, { animate_node: content_node });
- expander.setUp();
- }
- });
- });
- "/>
<tal:form condition="view/displayEditForm">
<tr
=== 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-02 04:10:43 +0000
@@ -59,28 +59,7 @@
</tal:not-editable>
</tal:affects-me-too>
-<table
- id="affected-software"
- tal:attributes="class python: context.duplicateof and 'duplicate listing' or 'listing'"
->
- <thead>
- <tr>
- <th colspan="2">Affects</th>
- <th>Status</th>
- <th>Importance</th>
- <th>Assigned to</th>
- <th>Milestone</th>
- </tr>
- </thead>
-
- <tbody>
- <tal:bugtask-or-nomination
- repeat="task_or_nom_view view/getBugTaskAndNominationViews">
- <tal:block replace="structure task_or_nom_view" />
- </tal:bugtask-or-nomination>
- </tbody>
-
-</table>
+<tal:bugtask_table replace="structure context/@@+bugtasks-and-nominations-table" />
<div class="actions"
tal:define="current_bugtask view/current_bugtask"
=== 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-02 04:10:43 +0000
@@ -0,0 +1,27 @@
+<tal:root
+ xmlns:tal="http://xml.zope.org/namespaces/tal"
+ xmlns:metal="http://xml.zope.org/namespaces/metal"
+ omit-tag="">
+ <table
+ id="affected-software"
+ tal:attributes="class python: context.duplicateof and 'duplicate listing' or 'listing'"
+ >
+ <thead>
+ <tr>
+ <th colspan="2">Affects</th>
+ <th>Status</th>
+ <th>Importance</th>
+ <th>Assigned to</th>
+ <th>Milestone</th>
+ </tr>
+ </thead>
+
+ <tbody>
+ <tal:bugtask-or-nomination
+ repeat="task_or_nom_view view/getBugTaskAndNominationViews">
+ <tal:block replace="structure task_or_nom_view" />
+ </tal:bugtask-or-nomination>
+ </tbody>
+
+ </table>
+</tal:root>
Follow ups