Merge lp:~wallyworld/launchpad/show-ajax-notifications into lp:launchpad

Proposed by Ian Booth
Status: Merged
Approved by: Ian Booth
Approved revision: no longer in the source branch.
Merged at revision: 12738
Proposed branch: lp:~wallyworld/launchpad/show-ajax-notifications
Merge into: lp:launchpad
Diff against target: 268 lines (+130/-9)
10 files modified
lib/canonical/launchpad/webapp/configure.zcml (+5/-0)
lib/canonical/launchpad/webapp/servers.py (+0/-1)
lib/lp/app/javascript/client.js (+63/-0)
lib/lp/app/javascript/tests/test_lp_client.html (+3/-1)
lib/lp/app/javascript/tests/test_lp_client.js (+47/-0)
lib/lp/bugs/stories/webservice/xx-bug.txt (+5/-0)
lib/lp/bugs/tests/test_bugs_webservice.py (+2/-2)
lib/lp/code/model/tests/test_branchset.py (+1/-1)
lib/lp/registry/tests/test_person.py (+2/-2)
versions.cfg (+2/-2)
To merge this branch: bzr merge lp:~wallyworld/launchpad/show-ajax-notifications
Reviewer Review Type Date Requested Status
Henning Eggers (community) code Approve
Review via email: mp+54342@code.launchpad.net

Commit message

[r=henninge][no-qa] Display notifications that may be included in an xhr response object. These are of type informational, error, warning or debug.

Description of the change

Display notifications that may be included in an xhr response object (as we use for things like "Recipes are in beta", "Thanks for submitting a bug report" etc). These are of type informational, error, warning or debug.
This branch uses new functionality added to lazr.restful in branch lp:~wallyworld/lazr.restful/propogate-notifications

== Implementation ==

Notifications generated resulting from page loads are rendered using a tales macro. The HTML is inserted under the "context-publication" div element. This new Javascript version grabs the "context-publication" div and creates the new equivalent DOM elements.

When a lazr restful call is made via client.js, any notifications added to the HTTP response by the server using the addNotification() call will be packaged up and sent back using the X-Lazr-Notifications header. The on_success wrapper used by the lazr restful stuff to package the result resource calls a new display_notifications() method.

This branch is a prerequisite for the real reason for adding this functionality: lp:~wallyworld/launchpad/assign-non-contributor to fix bug 698138

== Tests ==

Added new javascript unit test case to test_lp_client.js. Tests:
  test_display_notifications

== Lint ==

Linting changed files:
  versions.cfg
  lib/canonical/launchpad/webapp/configure.zcml
  lib/canonical/launchpad/webapp/servers.py
  lib/lp/app/javascript/client.js
  lib/lp/app/javascript/tests/test_lp_client.html
  lib/lp/app/javascript/tests/test_lp_client.js

./versions.cfg
     241: Line exceeds 78 characters.
./lib/lp/app/javascript/client.js
       4: Line exceeds 78 characters.
     191: Line exceeds 78 characters.
     566: Line exceeds 78 characters.
     614: Line exceeds 78 characters.
     741: Line exceeds 78 characters.
     743: Line exceeds 78 characters.
./lib/lp/app/javascript/tests/test_lp_client.js
      22: Line exceeds 78 characters.
      23: Line exceeds 78 characters.
      25: Line exceeds 78 characters.
      38: Line exceeds 78 characters.

To post a comment you must log in.
Revision history for this message
Henning Eggers (henninge) wrote :
Download full text (7.8 KiB)

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

Hi Ian,
I am doing this review although I am not an experienced Javascript reviewer or
coder. I am hoping, hat this will help to change that. ;-) Feel free to
request another review.

A note on the MP itself: You mixed up commit message and the description. You
should swap that back. ;-)

I will mark this needs-information for now just because I have so many
questions. ;-)

 review needs-information

Cheers,
Henning

Am 28.03.2011 04:09, schrieb Ian Booth:
> === modified file 'lib/canonical/launchpad/webapp/configure.zcml'
> === modified file 'lib/canonical/launchpad/webapp/servers.py'
> === modified file 'lib/lp/app/javascript/client.js'
> --- lib/lp/app/javascript/client.js 2011-02-25 01:54:00 +0000
> +++ lib/lp/app/javascript/client.js 2011-03-28 01:50:57 +0000
> @@ -242,6 +242,8 @@
> var media_type = response.getResponseHeader('Content-Type');
> if (media_type.substring(0,16) == 'application/json') {
> representation = Y.JSON.parse(response.responseText);
> + display_notifications(
> + response.getResponseHeader('X-Lazr-Notifications'));
> wrapped = client.wrap_resource(uri, representation);
> result = old_on_success(wrapped);
> if (update_cache) {
> @@ -254,6 +256,73 @@
> }
> };
>
> +/**
> + * Display a list of notifications - error, warning, informational or debug.
> + * @param notifications An json encoded array of (level, message) tuples.
> + */
> +function display_notifications(notifications) {
> + if (notifications === undefined)
> + return;
> +
> + // First remove any existing notifications.
> + function remove_notifications(div_class) {
> + var nodes = Y.all('div.'+div_class);
> + nodes.each(function(node) {
> + var parent = node.get('parentNode');
> + parent.removeChild(node);
> + });
> + }
> + remove_notifications('error');
> + remove_notifications('warning');
> + remove_notifications('informational');
> + remove_notifications('debug');

I am a bit worried that this might accidentally remove more than wanted. Class
names like "error" and "warning" seem so general that somebody might use them
locally. How about "lp-error", "lp-warning" etc. ?

> +
> + // Now display the new ones.
> + notifications = Y.JSON.parse(notifications);
> + var warnings = new Array();
> + var errors = new Array();
> + var infos = new Array();
> + var debugs = new Array();
> +
> + Y.each(notifications, function(notification, key) {
> + var level = notification[0];
> + var message = notification[1];
> + switch (level) {
> + case 10:
> + debugs.push(message);
> + break;
> + case 20:
> + infos.push(message);
> + break;
> + case 30:
> + warnings.push(message);
> + break;
> + case 40:
> + errors.push(message);
> + break;
> + }
> + });
> + // The place where we want to insert the notification divs.
> + var last_messag...

Read more...

review: Needs Information
Revision history for this message
Ian Booth (wallyworld) wrote :
Download full text (5.8 KiB)

Hi Henning

Thanks for the review.

> I am doing this review although I am not an experienced Javascript reviewer or
> coder. I am hoping, hat this will help to change that. ;-) Feel free to
> request another review.
>

No problem. I'm happy to discuss this mp with you. I'm also relatively
inexperienced with javascript.

> A note on the MP itself: You mixed up commit message and the description. You
> should swap that back. ;-)
>

Oh dear. What was I smoking.

> I will mark this needs-information for now just because I have so many
> questions. ;-)
>

It may help to know that the javascript in this mp is designed to
replicate the existing functionality provided by the "notifications"
macro defined in base-layout-macros.pt. The tales macros provide the
functionality for when page loads occur and this mp provides the same
functionality for when inline edits occur.

>> + remove_notifications('error');
>> + remove_notifications('warning');
>> + remove_notifications('informational');
>> + remove_notifications('debug');
>
> I am a bit worried that this might accidentally remove more than wanted. Class
> names like "error" and "warning" seem so general that somebody might use them
> locally. How about "lp-error", "lp-warning" etc. ?
>
>

These css class names are already defined in the tales macro. However,
they are actually "error message", "warning message" etc. The code works
as is since the matching infrastructure looks for "error.*" etc. There's
issues with constructing a jquery selector for class names with spaces
that I haven't worked out yet. I meant to do this before the mp was
submitted but forgot. Thanks for pointing this out. I'll fix it.

>> + // The place where we want to insert the notification divs.
>> + var last_message = Y.one('div.context-publication');
>> + var display_notification = function(notification, div_class) {
>> + var node = Y.Node.create("<div class='"+div_class+"'/>");
>> + node.set('innerHTML', notification);
>> + last_message.insert(node, 'after');
>> + last_message = node;
>> + };
>
> Since the cover letter is missing the pre-imp information, I have to ask:
> Why append these here? I would have expected that the template defines a place
> by naming a special div, e.g. <div class="lp-notifications">. They are placed
>

The implementation in this mp is as per the existing tales macro and
where it places the various divs containing the messages.

 very loosely on the page. Or do you expect that warnings might
displayed in a
> different place than, say debug messages?
> Also, putting them in a div would make it easy to implement the removal by
> simply removing the content of that div.
>

The debug, warning, info, error messages are displayed one under the
other inside the "context-publication" div.

>
>> + Y.each(errors, function(notification, key) {
>> + display_notification(notification, 'error message');
>> + });
>> + Y.each(warnings, function(notification, key) {
>> + display_notification(notification, 'warning message');
>> + });
>> + Y.each(infos, function(notification, key) {
>> + display_notification(notification, 'i...

Read more...

Revision history for this message
Ian Booth (wallyworld) wrote :

I've now implemented the requested changes.

Revision history for this message
Henning Eggers (henninge) wrote :

> >> + remove_notifications('error');
> >> + remove_notifications('warning');
> >> + remove_notifications('informational');
> >> + remove_notifications('debug');
> >
> > I am a bit worried that this might accidentally remove more than wanted.
> Class
> > names like "error" and "warning" seem so general that somebody might use
> them
> > locally. How about "lp-error", "lp-warning" etc. ?
> >
> >
>
> These css class names are already defined in the tales macro. However,
> they are actually "error message", "warning message" etc. The code works
> as is since the matching infrastructure looks for "error.*" etc. There's
> issues with constructing a jquery selector for class names with spaces
> that I haven't worked out yet. I meant to do this before the mp was
> submitted but forgot. Thanks for pointing this out. I'll fix it.

Thanks for the fix. I still think that the names are pretty generic but I guess that is a general problem and we should define a global "lp-" prefix. I just read about YUI's support for automatic class prefixes.

>
>
> >> + // The place where we want to insert the notification divs.
> >> + var last_message = Y.one('div.context-publication');
> >> + var display_notification = function(notification, div_class) {
> >> + var node = Y.Node.create("<div class='"+div_class+"'/>");
> >> + node.set('innerHTML', notification);
> >> + last_message.insert(node, 'after');
> >> + last_message = node;
> >> + };
> The debug, warning, info, error messages are displayed one under the
> other inside the "context-publication" div.

Sorry, I had not looked at what the "publication" diff is. But to correct you, they are displayed *under* the publication. That is obviously the right place for them, so all is fine here.

> > I wonder if all these repetitions could not be placed in loops in some way.
> > That would also make it easier to add more levels, if needed.
> >
>
> Perhaps. It's fairly easy to understand as is and setting stuff up so it
> can be done in a loop will maybe add more boilerplate around it all.
> I'll see what can be done.

Ah, I see you introduced notification_info. That is a good pattern and you could have followed that from the start. ;-) Have a look at this branch to see what I have in mind, although I have not run the code yet.
lp:~henninge/launchpad/wallyworld-show-ajax-notifications-refactor

I don't insist on you using this but it would be more consistent.

That's all. Thank you for your patience. I really learned from this review.

Henning

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/canonical/launchpad/webapp/configure.zcml'
--- lib/canonical/launchpad/webapp/configure.zcml 2011-02-03 19:01:10 +0000
+++ lib/canonical/launchpad/webapp/configure.zcml 2011-04-04 03:15:46 +0000
@@ -58,6 +58,11 @@
58 provides="lazr.restful.interfaces.IWebBrowserOriginatingRequest"58 provides="lazr.restful.interfaces.IWebBrowserOriginatingRequest"
59 factory="canonical.launchpad.webapp.servers.web_service_request_to_browser_request"59 factory="canonical.launchpad.webapp.servers.web_service_request_to_browser_request"
60 />60 />
61 <adapter
62 for="canonical.launchpad.layers.WebServiceLayer"
63 provides="lazr.restful.interfaces.INotificationsProvider"
64 factory="canonical.launchpad.webapp.interfaces.INotificationRequest"
65 />
6166
62 <!-- lazr.batchnavigator hook -->67 <!-- lazr.batchnavigator hook -->
63 <adapter68 <adapter
6469
=== modified file 'lib/canonical/launchpad/webapp/servers.py'
--- lib/canonical/launchpad/webapp/servers.py 2010-12-06 14:59:43 +0000
+++ lib/canonical/launchpad/webapp/servers.py 2011-04-04 03:15:46 +0000
@@ -21,7 +21,6 @@
21 WebServiceRequestTraversal,21 WebServiceRequestTraversal,
22 )22 )
23from lazr.uri import URI23from lazr.uri import URI
24import pytz
25import transaction24import transaction
26from transaction.interfaces import ISynchronizer25from transaction.interfaces import ISynchronizer
27from zc.zservertracelog.tracelog import Server as ZServerTracelogServer26from zc.zservertracelog.tracelog import Server as ZServerTracelogServer
2827
=== modified file 'lib/lp/app/javascript/client.js'
--- lib/lp/app/javascript/client.js 2011-04-01 01:54:45 +0000
+++ lib/lp/app/javascript/client.js 2011-04-04 03:15:46 +0000
@@ -244,6 +244,8 @@
244 var media_type = response.getResponseHeader('Content-Type');244 var media_type = response.getResponseHeader('Content-Type');
245 if (media_type.substring(0,16) == 'application/json') {245 if (media_type.substring(0,16) == 'application/json') {
246 representation = Y.JSON.parse(response.responseText);246 representation = Y.JSON.parse(response.responseText);
247 display_notifications(
248 response.getResponseHeader('X-Lazr-Notifications'));
247 wrapped = client.wrap_resource(uri, representation);249 wrapped = client.wrap_resource(uri, representation);
248 result = old_on_success(wrapped);250 result = old_on_success(wrapped);
249 if (update_cache) {251 if (update_cache) {
@@ -256,6 +258,67 @@
256 }258 }
257};259};
258260
261/**
262 * Display a list of notifications - error, warning, informational or debug.
263 * @param notifications An json encoded array of (level, message) tuples.
264 */
265function display_notifications(notifications) {
266 if (notifications === undefined)
267 return;
268
269 var notification_info = {
270 'level10': {
271 'notifications': new Array(),
272 'selector': '.debug.message',
273 'css_class': 'debug message'
274 },
275 'level20': {
276 'notifications': new Array(),
277 'selector': '.informational.message',
278 'css_class': 'informational message'
279 },
280 'level30': {
281 'notifications': new Array(),
282 'selector': '.warning.message',
283 'css_class': 'warning message'
284 },
285 'level40': {
286 'notifications': new Array(),
287 'selector': '.error.message',
288 'css_class': 'error message'
289 }
290 };
291
292 // First remove any existing notifications.
293 Y.each(notification_info, function (info) {
294 var nodes = Y.all('div'+info.selector);
295 nodes.each(function(node) {
296 var parent = node.get('parentNode');
297 parent.removeChild(node);
298 });
299 });
300
301 // Now display the new ones.
302 notifications = Y.JSON.parse(notifications);
303 Y.each(notifications, function(notification, key) {
304 var level = notification[0];
305 var message = notification[1];
306 var info = notification_info['level'+level];
307 info.notifications.push(message);
308 });
309
310 // The place where we want to insert the notification divs.
311 var last_message = Y.one('div.context-publication');
312 // A mapping from the div class to notification messages.
313 Y.each(notification_info, function(info) {
314 Y.each(info.notifications, function(notification) {
315 var node = Y.Node.create("<div class='"+info.css_class+"'/>");
316 node.set('innerHTML', notification);
317 last_message.insert(node, 'after');
318 last_message = node;
319 });
320 });
321}
259322
260// The resources that come together to make Launchpad.323// The resources that come together to make Launchpad.
261324
262325
=== modified file 'lib/lp/app/javascript/tests/test_lp_client.html'
--- lib/lp/app/javascript/tests/test_lp_client.html 2011-02-25 01:54:00 +0000
+++ lib/lp/app/javascript/tests/test_lp_client.html 2011-04-04 03:15:46 +0000
@@ -18,7 +18,9 @@
18 <script type="text/javascript" src="test_lp_client.js"></script>18 <script type="text/javascript" src="test_lp_client.js"></script>
19</head>19</head>
20<body class="yui3-skin-sam">20<body class="yui3-skin-sam">
21 <div id="container-of-stuff">21 <div class="context-publication">
22 <div id="container-of-stuff">
23 </div>
22 </div>24 </div>
23</body>25</body>
24</html>26</html>
2527
=== modified file 'lib/lp/app/javascript/tests/test_lp_client.js'
--- lib/lp/app/javascript/tests/test_lp_client.js 2011-04-01 01:54:45 +0000
+++ lib/lp/app/javascript/tests/test_lp_client.js 2011-04-04 03:15:46 +0000
@@ -187,6 +187,53 @@
187 }187 }
188}));188}));
189189
190function MockHttpResponse () {
191 this.responseText = '[]';
192 this.responseHeaders = {};
193}
194
195MockHttpResponse.prototype = {
196 setResponseHeader: function (header, value) {
197 this.responseHeaders[header] = value;
198 },
199
200 getResponseHeader: function(header) {
201 return this.responseHeaders[header];
202 }
203};
204
205suite.add(new Y.Test.Case({
206 name: "lp.client.notifications",
207
208 setUp: function() {
209 this.client = new Y.lp.client.Launchpad();
210 this.args=[this.client, null, this._on_success, false];
211 this.response = new MockHttpResponse();
212 this.response.setResponseHeader('Content-Type', 'application/json');
213 },
214
215 _on_success: function(entry) {
216 },
217
218 _checkNotificationNode: function(node_class, node_text) {
219 var node = Y.one('div'+node_class);
220 Assert.areEqual(node_text, node.get("innerHTML"));
221 },
222
223 test_display_notifications: function() {
224 var notifications = '[ [10, "A debug"], [20, "An info"], ' +
225 '[30, "A warning"], [40, "An error"] ]';
226 this.response.setResponseHeader(
227 'X-Lazr-Notifications', notifications);
228 Y.lp.client.wrap_resource_on_success(null, this.response, this.args);
229 this._checkNotificationNode('.debug.message', 'A debug');
230 this._checkNotificationNode('.informational.message', 'An info');
231 this._checkNotificationNode('.warning.message', 'A warning');
232 this._checkNotificationNode('.error.message', 'An error');
233 }
234
235}));
236
190237
191// Lock, stock, and two smoking barrels.238// Lock, stock, and two smoking barrels.
192var handle_complete = function(data) {239var handle_complete = function(data) {
193240
=== modified file 'lib/lp/bugs/stories/webservice/xx-bug.txt'
--- lib/lp/bugs/stories/webservice/xx-bug.txt 2011-02-13 22:32:30 +0000
+++ lib/lp/bugs/stories/webservice/xx-bug.txt 2011-04-04 03:15:46 +0000
@@ -434,6 +434,11 @@
434 >>> print webservice.get(bugtask_path).jsonBody()['milestone_link']434 >>> print webservice.get(bugtask_path).jsonBody()['milestone_link']
435 http://.../debian/+milestone/3.1435 http://.../debian/+milestone/3.1
436436
437We need to ensure the milestone we try and set is different to the current
438value because lazr restful now discards attempts to patch an attribute with an
439unchanged value.
440 >>> patch = {u'milestone_link': webservice.getAbsoluteUrl(
441 ... '/debian/+milestone/3.1-rc1')}
437 >>> print user_webservice.patch(442 >>> print user_webservice.patch(
438 ... bugtask_path, 'application/json', dumps(patch))443 ... bugtask_path, 'application/json', dumps(patch))
439 HTTP/1.1 401 Unauthorized...444 HTTP/1.1 401 Unauthorized...
440445
=== modified file 'lib/lp/bugs/tests/test_bugs_webservice.py'
--- lib/lp/bugs/tests/test_bugs_webservice.py 2011-03-23 16:28:51 +0000
+++ lib/lp/bugs/tests/test_bugs_webservice.py 2011-04-04 03:15:46 +0000
@@ -205,7 +205,7 @@
205 store.flush()205 store.flush()
206 store.reset()206 store.reset()
207 response = webservice.get(url)207 response = webservice.get(url)
208 self.assertThat(collector, HasQueryCount(LessThan(23)))208 self.assertThat(collector, HasQueryCount(LessThan(24)))
209 with_2_count = collector.count209 with_2_count = collector.count
210 self.failUnlessEqual(response.status, 200)210 self.failUnlessEqual(response.status, 200)
211 login(USER_EMAIL)211 login(USER_EMAIL)
@@ -241,7 +241,7 @@
241 store.flush()241 store.flush()
242 store.reset()242 store.reset()
243 response = webservice.get(url)243 response = webservice.get(url)
244 self.assertThat(collector, HasQueryCount(LessThan(23)))244 self.assertThat(collector, HasQueryCount(LessThan(24)))
245 with_2_count = collector.count245 with_2_count = collector.count
246 self.failUnlessEqual(response.status, 200)246 self.failUnlessEqual(response.status, 200)
247 login(USER_EMAIL)247 login(USER_EMAIL)
248248
=== modified file 'lib/lp/code/model/tests/test_branchset.py'
--- lib/lp/code/model/tests/test_branchset.py 2011-03-03 01:13:47 +0000
+++ lib/lp/code/model/tests/test_branchset.py 2011-04-04 03:15:46 +0000
@@ -57,4 +57,4 @@
57 self.assertEqual(response.status, 200,57 self.assertEqual(response.status, 200,
58 "Got %d for url %r with response %r" % (58 "Got %d for url %r with response %r" % (
59 response.status, url, response.body))59 response.status, url, response.body))
60 self.assertThat(collector, HasQueryCount(LessThan(13)))60 self.assertThat(collector, HasQueryCount(LessThan(17)))
6161
=== modified file 'lib/lp/registry/tests/test_person.py'
--- lib/lp/registry/tests/test_person.py 2011-04-01 00:47:03 +0000
+++ lib/lp/registry/tests/test_person.py 2011-04-04 03:15:46 +0000
@@ -1293,10 +1293,10 @@
1293 self.assertEqual(response.status, 200,1293 self.assertEqual(response.status, 200,
1294 "Got %d for url %r with response %r" % (1294 "Got %d for url %r with response %r" % (
1295 response.status, url, response.body))1295 response.status, url, response.body))
1296 # XXX: This number should really be 10, but see1296 # XXX: This number should really be 12, but see
1297 # https://bugs.launchpad.net/storm/+bug/619017 which is adding 31297 # https://bugs.launchpad.net/storm/+bug/619017 which is adding 3
1298 # queries to the test.1298 # queries to the test.
1299 self.assertThat(collector, HasQueryCount(LessThan(14)))1299 self.assertThat(collector, HasQueryCount(LessThan(16)))
13001300
13011301
1302class TestGetRecipients(TestCaseWithFactory):1302class TestGetRecipients(TestCaseWithFactory):
13031303
=== modified file 'versions.cfg'
--- versions.cfg 2011-03-28 00:27:00 +0000
+++ versions.cfg 2011-04-04 03:15:46 +0000
@@ -34,7 +34,7 @@
34lazr.delegates = 1.2.034lazr.delegates = 1.2.0
35lazr.enum = 1.1.235lazr.enum = 1.1.2
36lazr.lifecycle = 1.136lazr.lifecycle = 1.1
37lazr.restful = 0.18.037lazr.restful = 0.18.1
38lazr.restfulclient = 0.11.238lazr.restfulclient = 0.11.2
39lazr.smtptest = 1.139lazr.smtptest = 1.1
40lazr.testing = 0.1.140lazr.testing = 0.1.1
@@ -66,7 +66,7 @@
66roman = 1.4.066roman = 1.4.0
67setproctitle = 1.067setproctitle = 1.0
68setuptools = 0.6c1168setuptools = 0.6c11
69simplejson = 2.0.969simplejson = 2.1.3
70simplesettings = 0.470simplesettings = 0.4
71SimpleTal = 4.171SimpleTal = 4.1
72Sphinx = 1.0.772Sphinx = 1.0.7