Merge lp://staging/~intellectronica/launchpad/bugtask-ajax-target into lp://staging/launchpad

Proposed by Eleanor Berger
Status: Merged
Merged at revision: not available
Proposed branch: lp://staging/~intellectronica/launchpad/bugtask-ajax-target
Merge into: lp://staging/launchpad
Diff against target: None lines
To merge this branch: bzr merge lp://staging/~intellectronica/launchpad/bugtask-ajax-target
Reviewer Review Type Date Requested Status
Paul Hummer (community) ui Approve
Gavin Panella (community) code Approve
Review via email: mp+10726@code.staging.launchpad.net
To post a comment you must log in.
Revision history for this message
Eleanor Berger (intellectronica) wrote :

This branch implements an ajax product picker, which allows the user to change the target of a bug inline. It uses the picker infrastructure added by Edwin (for assignee, originally) though I had to make some modifications to make it work.

 * The picker now works with the new entry format returned from the server. The results is now formatted using XHTML so extracting the rendered value is a bit more complicated. This code can probably be factored out later for general use.

 * Because the canonical_url of a bugtask depends on its target, after the target changes the resource to patch changes to. To make that happen I had to resort to hack in which I keep the URI in a field of the picker, instead of using a closure, so that it can be changed later. Ideally, this should be done with proper YUI3 attributes and exposed using events, but that's beyond the scope of this branch and I don't want to block on it.

 * I have also had to add a formatter adapter for rendering the xhtml representation of a bug target, and fix a problem with the assignee formatter where it would bomb if assignee is None (all formatters are now being used for all attributes when the xhtml representation is required, so the attribute might be None if the task is unassigned).

To test, go to a bug with a product task and change the target.

Revision history for this message
Gavin Panella (allenap) wrote :
Download full text (12.6 KiB)

Hi Tom,

I have a lot of comments about this branch, but I think it's a nice
feature and it works well.

Gavin.

> === modified file 'lib/canonical/launchpad/javascript/bugs/bugtask-index.js'
> --- lib/canonical/launchpad/javascript/bugs/bugtask-index.js 2009-08-25 10:55:16 +0000
> +++ lib/canonical/launchpad/javascript/bugs/bugtask-index.js 2009-08-26 11:02:06 +0000
> @@ -1208,6 +1208,7 @@
> }
>
> var tr = Y.get('#' + conf.row_id);
> + var bugtarget_content = Y.get('#bugtarget-picker-' + conf.row_id);
> var status_content = tr.query('.status-content');
> var importance_content = tr.query('.importance-content');
> var milestone_content = tr.query('.milestone-content');
> @@ -1223,6 +1224,20 @@
> }
>
> if ((LP.client.links.me !== undefined) && (LP.client.links.me !== null)) {
> + if (Y.Lang.isValue(bugtarget_content)) {
> + if (conf.target_is_product) {
> + var bugtarget_picker = Y.lp.picker.addPickerPatcher(
> + 'Product',
> + conf.bugtask_path,
> + "target_link",
> + bugtarget_content.get('id'),
> + true,
> + false,
> + {"step_title": "Search products",
> + "header": "Change product"});
> + }
> + }
> +
> if (conf.user_can_edit_status) {
> var status_choice_edit = new Y.ChoiceSource({
> contentBox: status_content,
>
> === modified file 'lib/canonical/launchpad/javascript/lp/picker.js'
> --- lib/canonical/launchpad/javascript/lp/picker.js 2009-08-05 14:02:16 +0000
> +++ lib/canonical/launchpad/javascript/lp/picker.js 2009-08-26 11:02:06 +0000
> @@ -78,8 +78,23 @@
> var save = function (picker_result) {
> activator.renderProcessing();
> var success_handler = function (entry) {
> - var node = Y.Node.create(
> - '<span>' + entry.get('assignee_link') + '</span>');
> + var xhtml = Y.DOM.create(entry)[1];

This is fragile; the reason is that the first element returned from
Y.DOM.create() is a text node containing "\n\n", which is the text
after the XML PI and the start of the dl. If the rendering of this
ever changes it'll break.

The following strips the XML PI to demonstrate the problem.

    entry = entry.replace(/<[?].+[?]>/, "");
    var xhtml = Y.DOM.create(entry); // No [1]

It seems that Y.DOM.create considers whitespace after the PI to be
significant, while unenclosed whitespace is not. I don't know if this
is correct behaviour, but it's annoying.

Perhaps it's possible to change the XML renderer to remove this
whitespace? Or add a test to make sure it's always there? Mmm, but
even then, I just get a gut feeling that this is not behaviour we
should rely upon in YUI; it might change. Gah! Software is sh*t :)

> + var current_field = null;
> + for (var i = 0; i < xhtml.childNodes.length; i++) {
> + var element = xhtml.childNodes[i];
> + if (element.tagName == 'DT') {
> + current_field = element.innerHTML;

We control this field, so I gu...

review: Needs Fixing (code)
Revision history for this message
Gavin Panella (allenap) wrote :

Tom said on IRC:

  "i appreciate that the code for extracting the xhtml is ugly (i
   even wrote so in my mp), but since it works, and since we
   control both the renderer and the parser, i think it's fine to
   defer improving it until we write a nice general purpose
   extractor later on."

  "i also don't think it's worth adding code for handling
   in-the-air collisions. we never do"

Which is all fine with me.

So, with the changes at http://pastebin.ubuntu.com/259896/ in addition
to a webservice test for the addition of the mutator_for() decorator
to transitionToTarget(), merge approved :)

Thanks!

review: Approve (code)
Revision history for this message
Paul Hummer (rockstar) wrote :

After filing bug 419489 and the removal of the "remove" link, I'm happy with this branch.

review: Approve (ui)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/canonical/launchpad/javascript/bugs/bugtask-index.js'
--- lib/canonical/launchpad/javascript/bugs/bugtask-index.js 2009-08-25 10:55:16 +0000
+++ lib/canonical/launchpad/javascript/bugs/bugtask-index.js 2009-08-26 10:38:56 +0000
@@ -1208,6 +1208,7 @@
1208 }1208 }
12091209
1210 var tr = Y.get('#' + conf.row_id);1210 var tr = Y.get('#' + conf.row_id);
1211 var bugtarget_content = Y.get('#bugtarget-picker-' + conf.row_id);
1211 var status_content = tr.query('.status-content');1212 var status_content = tr.query('.status-content');
1212 var importance_content = tr.query('.importance-content');1213 var importance_content = tr.query('.importance-content');
1213 var milestone_content = tr.query('.milestone-content');1214 var milestone_content = tr.query('.milestone-content');
@@ -1223,6 +1224,20 @@
1223 }1224 }
12241225
1225 if ((LP.client.links.me !== undefined) && (LP.client.links.me !== null)) {1226 if ((LP.client.links.me !== undefined) && (LP.client.links.me !== null)) {
1227 if (Y.Lang.isValue(bugtarget_content)) {
1228 if (conf.target_is_product) {
1229 var bugtarget_picker = Y.lp.picker.addPickerPatcher(
1230 'Product',
1231 conf.bugtask_path,
1232 "target_link",
1233 bugtarget_content.get('id'),
1234 true,
1235 false,
1236 {"step_title": "Search products",
1237 "header": "Change product"});
1238 }
1239 }
1240
1226 if (conf.user_can_edit_status) {1241 if (conf.user_can_edit_status) {
1227 var status_choice_edit = new Y.ChoiceSource({1242 var status_choice_edit = new Y.ChoiceSource({
1228 contentBox: status_content,1243 contentBox: status_content,
12291244
=== modified file 'lib/canonical/launchpad/javascript/lp/picker.js'
--- lib/canonical/launchpad/javascript/lp/picker.js 2009-08-05 14:02:16 +0000
+++ lib/canonical/launchpad/javascript/lp/picker.js 2009-08-26 10:04:41 +0000
@@ -78,8 +78,23 @@
78 var save = function (picker_result) {78 var save = function (picker_result) {
79 activator.renderProcessing();79 activator.renderProcessing();
80 var success_handler = function (entry) {80 var success_handler = function (entry) {
81 var node = Y.Node.create(81 var xhtml = Y.DOM.create(entry)[1];
82 '<span>' + entry.get('assignee_link') + '</span>');82 var current_field = null;
83 for (var i = 0; i < xhtml.childNodes.length; i++) {
84 var element = xhtml.childNodes[i];
85 if (element.tagName == 'DT') {
86 current_field = element.innerHTML;
87 continue;
88 }
89 if (element.tagName == 'DD') {
90 if (current_field == attribute_name) {
91 // The field value is found
92 node = Y.get(element).query('span');
93 } else if (current_field == 'self_link') {
94 picker._resource_uri = element.innerHTML;
95 }
96 }
97 }
83 activator.renderSuccess(node);98 activator.renderSuccess(node);
84 show_hide_buttons();99 show_hide_buttons();
85 };100 };
@@ -89,7 +104,7 @@
89 picker_result.api_uri);104 picker_result.api_uri);
90105
91 var client = new LP.client.Launchpad();106 var client = new LP.client.Launchpad();
92 client.patch(resource_uri, patch_payload, {107 client.patch(picker._resource_uri, patch_payload, {
93 accept: 'application/xhtml+xml',108 accept: 'application/xhtml+xml',
94 on: {109 on: {
95 success: success_handler,110 success: success_handler,
@@ -119,7 +134,7 @@
119 patch_payload[attribute_name] = null;134 patch_payload[attribute_name] = null;
120135
121 var client = new LP.client.Launchpad();136 var client = new LP.client.Launchpad();
122 client.patch(resource_uri, patch_payload, {137 client.patch(picker._resource_uri, patch_payload, {
123 on: {138 on: {
124 success: success_handler,139 success: success_handler,
125 failure: failure_handler140 failure: failure_handler
@@ -128,6 +143,7 @@
128 };143 };
129144
130 var picker = Y.lp.picker.create(vocabulary, save, config);145 var picker = Y.lp.picker.create(vocabulary, save, config);
146 picker._resource_uri = resource_uri;
131 var extra_buttons = Y.Node.create(147 var extra_buttons = Y.Node.create(
132 '<div style="text-align: center; height: 3em; ' +148 '<div style="text-align: center; height: 3em; ' +
133 'white-space: nowrap"/>');149 'white-space: nowrap"/>');
@@ -158,6 +174,8 @@
158 activator.render();174 activator.render();
159175
160 show_hide_buttons();176 show_hide_buttons();
177
178 return picker;
161};179};
162180
163/**181/**
@@ -297,5 +315,5 @@
297}, '0.1', {315}, '0.1', {
298requires: [316requires: [
299 'io', 'dom', 'dump', 'lazr.picker', 'lazr.activator', 'json-parse',317 'io', 'dom', 'dump', 'lazr.picker', 'lazr.activator', 'json-parse',
300 'lp.client.helpers'318 'datatype-xml', 'lp.client.helpers'
301 ]});319 ]});
302320
=== modified file 'lib/lp/bugs/browser/bugtask.py'
--- lib/lp/bugs/browser/bugtask.py 2009-08-25 12:35:23 +0000
+++ lib/lp/bugs/browser/bugtask.py 2009-08-26 10:38:56 +0000
@@ -10,6 +10,7 @@
10 'BugListingBatchNavigator',10 'BugListingBatchNavigator',
11 'BugListingPortletView',11 'BugListingPortletView',
12 'BugNominationsView',12 'BugNominationsView',
13 'bugtarget_renderer',
13 'BugTargetTraversalMixin',14 'BugTargetTraversalMixin',
14 'BugTargetView',15 'BugTargetView',
15 'BugTaskContextMenu',16 'BugTaskContextMenu',
@@ -76,7 +77,7 @@
76from lazr.lifecycle.snapshot import Snapshot77from lazr.lifecycle.snapshot import Snapshot
77from lazr.restful.interface import copy_field78from lazr.restful.interface import copy_field
78from lazr.restful.interfaces import (79from lazr.restful.interfaces import (
79 IFieldHTMLRenderer, IReferenceChoice, IWebServiceClientRequest)80 IFieldHTMLRenderer, IReference, IReferenceChoice, IWebServiceClientRequest)
8081
81from canonical.config import config82from canonical.config import config
82from canonical.database.sqlbase import cursor83from canonical.database.sqlbase import cursor
@@ -137,7 +138,8 @@
137from canonical.launchpad.webapp.authorization import check_permission138from canonical.launchpad.webapp.authorization import check_permission
138from canonical.launchpad.webapp.batching import TableBatchNavigator139from canonical.launchpad.webapp.batching import TableBatchNavigator
139from canonical.launchpad.webapp.menu import structured140from canonical.launchpad.webapp.menu import structured
140from canonical.launchpad.webapp.tales import PersonFormatterAPI, FormattersAPI141from canonical.launchpad.webapp.tales import (
142 FormattersAPI, ObjectImageDisplayAPI, PersonFormatterAPI)
141143
142from canonical.lazr.interfaces import IObjectPrivacy144from canonical.lazr.interfaces import IObjectPrivacy
143from lazr.restful.interfaces import IJSONRequestCache145from lazr.restful.interfaces import IJSONRequestCache
@@ -161,7 +163,24 @@
161def assignee_renderer(context, field, request):163def assignee_renderer(context, field, request):
162 """Render a bugtask assignee as a link."""164 """Render a bugtask assignee as a link."""
163 def render(value):165 def render(value):
164 return PersonFormatterAPI(context.assignee).link('+assignedbugs')166 if context.assignee is None:
167 return ''
168 else:
169 return PersonFormatterAPI(context.assignee).link(None)
170 return render
171
172@component.adapter(IBugTask, IReference, IWebServiceClientRequest)
173@implementer(IFieldHTMLRenderer)
174def bugtarget_renderer(context, field, request):
175 """Render a bugtarget as a link."""
176 def render(value):
177 html = """<span>
178 <a href="%(href)s" class="%(class)s">%(displayname)s</a>
179 </span>""" % {
180 'href': canonical_url(context.target),
181 'class': ObjectImageDisplayAPI(context.target).sprite_css(),
182 'displayname': context.bugtargetdisplayname}
183 return html
165 return render184 return render
166185
167def unique_title(title):186def unique_title(title):
@@ -3151,6 +3170,7 @@
3151 'bugtask_path': '/'.join(3170 'bugtask_path': '/'.join(
3152 [''] + canonical_url(self.context).split('/')[3:]),3171 [''] + canonical_url(self.context).split('/')[3:]),
3153 'prefix': get_prefix(self.context),3172 'prefix': get_prefix(self.context),
3173 'target_is_product': IProduct.providedBy(self.context.target),
3154 'status_widget_items': self.status_widget_items,3174 'status_widget_items': self.status_widget_items,
3155 'status_value': self.context.status.title,3175 'status_value': self.context.status.title,
3156 'importance_widget_items': self.importance_widget_items,3176 'importance_widget_items': self.importance_widget_items,
31573177
=== modified file 'lib/lp/bugs/configure.zcml'
--- lib/lp/bugs/configure.zcml 2009-08-24 20:28:33 +0000
+++ lib/lp/bugs/configure.zcml 2009-08-25 13:51:39 +0000
@@ -270,11 +270,13 @@
270 factory="lp.bugs.browser.bugtask.assignee_renderer"270 factory="lp.bugs.browser.bugtask.assignee_renderer"
271 name="assignee"/>271 name="assignee"/>
272 <adapter272 <adapter
273 factory="lp.bugs.browser.bugtask.bugtarget_renderer"
274 name="target"/>
275 <adapter
273 provides="canonical.launchpad.webapp.interfaces.IBreadcrumb"276 provides="canonical.launchpad.webapp.interfaces.IBreadcrumb"
274 for="lp.bugs.interfaces.bugtask.IBugTask"277 for="lp.bugs.interfaces.bugtask.IBugTask"
275 factory="lp.bugs.browser.bugtask.BugTaskBreadcrumb"278 factory="lp.bugs.browser.bugtask.BugTaskBreadcrumb"
276 permission="zope.Public"/>279 permission="zope.Public"/>
277
278 280
279 <!-- NullBugTask -->281 <!-- NullBugTask -->
280 282
281283
=== modified file 'lib/lp/bugs/interfaces/bugtask.py'
--- lib/lp/bugs/interfaces/bugtask.py 2009-08-20 01:11:52 +0000
+++ lib/lp/bugs/interfaces/bugtask.py 2009-08-25 13:51:39 +0000
@@ -634,6 +634,7 @@
634 value is set to None, date_assigned is also set to None.634 value is set to None, date_assigned is also set to None.
635 """635 """
636636
637 @mutator_for(target)
637 @operation_parameters(638 @operation_parameters(
638 target=copy_field(target))639 target=copy_field(target))
639 @export_write_operation()640 @export_write_operation()
640641
=== modified file 'lib/lp/bugs/templates/bugtask-tasks-and-nominations-table-row.pt'
--- lib/lp/bugs/templates/bugtask-tasks-and-nominations-table-row.pt 2009-08-21 16:23:58 +0000
+++ lib/lp/bugs/templates/bugtask-tasks-and-nominations-table-row.pt 2009-08-26 10:38:56 +0000
@@ -36,12 +36,20 @@
36 </tal:not-conjoined-task>36 </tal:not-conjoined-task>
37 </td>37 </td>
38 <td tal:condition="not:indent_task">38 <td tal:condition="not:indent_task">
39 <a tal:attributes="39 <span tal:attributes="id string:bugtarget-picker-${row_id}">
40 href context/target/fmt:url;40 <span class="yui-activator-data-box">
41 title view/target_link_title;41 <span title="This project&rsquo;s license has not been specified.">
42 class context/target/image:sprite_css"42 <a tal:attributes="href context/target/fmt:url;
43 tal:content="context/bugtargetdisplayname"43 title view/target_link_title;
44 />44 class context/target/image:sprite_css"
45 tal:content="context/bugtargetdisplayname" />
46 </span>
47 </span>
48 <button class="lazr-btn yui-activator-act yui-activator-hidden">
49 Edit
50 </button>
51 <div class="yui-activator-message-box yui-activator-hidden" />
52 </span>
45 </td>53 </td>
4654
47 <tal:conjoined-task condition="is_conjoined_slave">55 <tal:conjoined-task condition="is_conjoined_slave">