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
1=== modified file 'lib/canonical/launchpad/webapp/configure.zcml'
2--- lib/canonical/launchpad/webapp/configure.zcml 2011-02-03 19:01:10 +0000
3+++ lib/canonical/launchpad/webapp/configure.zcml 2011-04-04 03:15:46 +0000
4@@ -58,6 +58,11 @@
5 provides="lazr.restful.interfaces.IWebBrowserOriginatingRequest"
6 factory="canonical.launchpad.webapp.servers.web_service_request_to_browser_request"
7 />
8+ <adapter
9+ for="canonical.launchpad.layers.WebServiceLayer"
10+ provides="lazr.restful.interfaces.INotificationsProvider"
11+ factory="canonical.launchpad.webapp.interfaces.INotificationRequest"
12+ />
13
14 <!-- lazr.batchnavigator hook -->
15 <adapter
16
17=== modified file 'lib/canonical/launchpad/webapp/servers.py'
18--- lib/canonical/launchpad/webapp/servers.py 2010-12-06 14:59:43 +0000
19+++ lib/canonical/launchpad/webapp/servers.py 2011-04-04 03:15:46 +0000
20@@ -21,7 +21,6 @@
21 WebServiceRequestTraversal,
22 )
23 from lazr.uri import URI
24-import pytz
25 import transaction
26 from transaction.interfaces import ISynchronizer
27 from zc.zservertracelog.tracelog import Server as ZServerTracelogServer
28
29=== modified file 'lib/lp/app/javascript/client.js'
30--- lib/lp/app/javascript/client.js 2011-04-01 01:54:45 +0000
31+++ lib/lp/app/javascript/client.js 2011-04-04 03:15:46 +0000
32@@ -244,6 +244,8 @@
33 var media_type = response.getResponseHeader('Content-Type');
34 if (media_type.substring(0,16) == 'application/json') {
35 representation = Y.JSON.parse(response.responseText);
36+ display_notifications(
37+ response.getResponseHeader('X-Lazr-Notifications'));
38 wrapped = client.wrap_resource(uri, representation);
39 result = old_on_success(wrapped);
40 if (update_cache) {
41@@ -256,6 +258,67 @@
42 }
43 };
44
45+/**
46+ * Display a list of notifications - error, warning, informational or debug.
47+ * @param notifications An json encoded array of (level, message) tuples.
48+ */
49+function display_notifications(notifications) {
50+ if (notifications === undefined)
51+ return;
52+
53+ var notification_info = {
54+ 'level10': {
55+ 'notifications': new Array(),
56+ 'selector': '.debug.message',
57+ 'css_class': 'debug message'
58+ },
59+ 'level20': {
60+ 'notifications': new Array(),
61+ 'selector': '.informational.message',
62+ 'css_class': 'informational message'
63+ },
64+ 'level30': {
65+ 'notifications': new Array(),
66+ 'selector': '.warning.message',
67+ 'css_class': 'warning message'
68+ },
69+ 'level40': {
70+ 'notifications': new Array(),
71+ 'selector': '.error.message',
72+ 'css_class': 'error message'
73+ }
74+ };
75+
76+ // First remove any existing notifications.
77+ Y.each(notification_info, function (info) {
78+ var nodes = Y.all('div'+info.selector);
79+ nodes.each(function(node) {
80+ var parent = node.get('parentNode');
81+ parent.removeChild(node);
82+ });
83+ });
84+
85+ // Now display the new ones.
86+ notifications = Y.JSON.parse(notifications);
87+ Y.each(notifications, function(notification, key) {
88+ var level = notification[0];
89+ var message = notification[1];
90+ var info = notification_info['level'+level];
91+ info.notifications.push(message);
92+ });
93+
94+ // The place where we want to insert the notification divs.
95+ var last_message = Y.one('div.context-publication');
96+ // A mapping from the div class to notification messages.
97+ Y.each(notification_info, function(info) {
98+ Y.each(info.notifications, function(notification) {
99+ var node = Y.Node.create("<div class='"+info.css_class+"'/>");
100+ node.set('innerHTML', notification);
101+ last_message.insert(node, 'after');
102+ last_message = node;
103+ });
104+ });
105+}
106
107 // The resources that come together to make Launchpad.
108
109
110=== modified file 'lib/lp/app/javascript/tests/test_lp_client.html'
111--- lib/lp/app/javascript/tests/test_lp_client.html 2011-02-25 01:54:00 +0000
112+++ lib/lp/app/javascript/tests/test_lp_client.html 2011-04-04 03:15:46 +0000
113@@ -18,7 +18,9 @@
114 <script type="text/javascript" src="test_lp_client.js"></script>
115 </head>
116 <body class="yui3-skin-sam">
117- <div id="container-of-stuff">
118+ <div class="context-publication">
119+ <div id="container-of-stuff">
120+ </div>
121 </div>
122 </body>
123 </html>
124
125=== modified file 'lib/lp/app/javascript/tests/test_lp_client.js'
126--- lib/lp/app/javascript/tests/test_lp_client.js 2011-04-01 01:54:45 +0000
127+++ lib/lp/app/javascript/tests/test_lp_client.js 2011-04-04 03:15:46 +0000
128@@ -187,6 +187,53 @@
129 }
130 }));
131
132+function MockHttpResponse () {
133+ this.responseText = '[]';
134+ this.responseHeaders = {};
135+}
136+
137+MockHttpResponse.prototype = {
138+ setResponseHeader: function (header, value) {
139+ this.responseHeaders[header] = value;
140+ },
141+
142+ getResponseHeader: function(header) {
143+ return this.responseHeaders[header];
144+ }
145+};
146+
147+suite.add(new Y.Test.Case({
148+ name: "lp.client.notifications",
149+
150+ setUp: function() {
151+ this.client = new Y.lp.client.Launchpad();
152+ this.args=[this.client, null, this._on_success, false];
153+ this.response = new MockHttpResponse();
154+ this.response.setResponseHeader('Content-Type', 'application/json');
155+ },
156+
157+ _on_success: function(entry) {
158+ },
159+
160+ _checkNotificationNode: function(node_class, node_text) {
161+ var node = Y.one('div'+node_class);
162+ Assert.areEqual(node_text, node.get("innerHTML"));
163+ },
164+
165+ test_display_notifications: function() {
166+ var notifications = '[ [10, "A debug"], [20, "An info"], ' +
167+ '[30, "A warning"], [40, "An error"] ]';
168+ this.response.setResponseHeader(
169+ 'X-Lazr-Notifications', notifications);
170+ Y.lp.client.wrap_resource_on_success(null, this.response, this.args);
171+ this._checkNotificationNode('.debug.message', 'A debug');
172+ this._checkNotificationNode('.informational.message', 'An info');
173+ this._checkNotificationNode('.warning.message', 'A warning');
174+ this._checkNotificationNode('.error.message', 'An error');
175+ }
176+
177+}));
178+
179
180 // Lock, stock, and two smoking barrels.
181 var handle_complete = function(data) {
182
183=== modified file 'lib/lp/bugs/stories/webservice/xx-bug.txt'
184--- lib/lp/bugs/stories/webservice/xx-bug.txt 2011-02-13 22:32:30 +0000
185+++ lib/lp/bugs/stories/webservice/xx-bug.txt 2011-04-04 03:15:46 +0000
186@@ -434,6 +434,11 @@
187 >>> print webservice.get(bugtask_path).jsonBody()['milestone_link']
188 http://.../debian/+milestone/3.1
189
190+We need to ensure the milestone we try and set is different to the current
191+value because lazr restful now discards attempts to patch an attribute with an
192+unchanged value.
193+ >>> patch = {u'milestone_link': webservice.getAbsoluteUrl(
194+ ... '/debian/+milestone/3.1-rc1')}
195 >>> print user_webservice.patch(
196 ... bugtask_path, 'application/json', dumps(patch))
197 HTTP/1.1 401 Unauthorized...
198
199=== modified file 'lib/lp/bugs/tests/test_bugs_webservice.py'
200--- lib/lp/bugs/tests/test_bugs_webservice.py 2011-03-23 16:28:51 +0000
201+++ lib/lp/bugs/tests/test_bugs_webservice.py 2011-04-04 03:15:46 +0000
202@@ -205,7 +205,7 @@
203 store.flush()
204 store.reset()
205 response = webservice.get(url)
206- self.assertThat(collector, HasQueryCount(LessThan(23)))
207+ self.assertThat(collector, HasQueryCount(LessThan(24)))
208 with_2_count = collector.count
209 self.failUnlessEqual(response.status, 200)
210 login(USER_EMAIL)
211@@ -241,7 +241,7 @@
212 store.flush()
213 store.reset()
214 response = webservice.get(url)
215- self.assertThat(collector, HasQueryCount(LessThan(23)))
216+ self.assertThat(collector, HasQueryCount(LessThan(24)))
217 with_2_count = collector.count
218 self.failUnlessEqual(response.status, 200)
219 login(USER_EMAIL)
220
221=== modified file 'lib/lp/code/model/tests/test_branchset.py'
222--- lib/lp/code/model/tests/test_branchset.py 2011-03-03 01:13:47 +0000
223+++ lib/lp/code/model/tests/test_branchset.py 2011-04-04 03:15:46 +0000
224@@ -57,4 +57,4 @@
225 self.assertEqual(response.status, 200,
226 "Got %d for url %r with response %r" % (
227 response.status, url, response.body))
228- self.assertThat(collector, HasQueryCount(LessThan(13)))
229+ self.assertThat(collector, HasQueryCount(LessThan(17)))
230
231=== modified file 'lib/lp/registry/tests/test_person.py'
232--- lib/lp/registry/tests/test_person.py 2011-04-01 00:47:03 +0000
233+++ lib/lp/registry/tests/test_person.py 2011-04-04 03:15:46 +0000
234@@ -1293,10 +1293,10 @@
235 self.assertEqual(response.status, 200,
236 "Got %d for url %r with response %r" % (
237 response.status, url, response.body))
238- # XXX: This number should really be 10, but see
239+ # XXX: This number should really be 12, but see
240 # https://bugs.launchpad.net/storm/+bug/619017 which is adding 3
241 # queries to the test.
242- self.assertThat(collector, HasQueryCount(LessThan(14)))
243+ self.assertThat(collector, HasQueryCount(LessThan(16)))
244
245
246 class TestGetRecipients(TestCaseWithFactory):
247
248=== modified file 'versions.cfg'
249--- versions.cfg 2011-03-28 00:27:00 +0000
250+++ versions.cfg 2011-04-04 03:15:46 +0000
251@@ -34,7 +34,7 @@
252 lazr.delegates = 1.2.0
253 lazr.enum = 1.1.2
254 lazr.lifecycle = 1.1
255-lazr.restful = 0.18.0
256+lazr.restful = 0.18.1
257 lazr.restfulclient = 0.11.2
258 lazr.smtptest = 1.1
259 lazr.testing = 0.1.1
260@@ -66,7 +66,7 @@
261 roman = 1.4.0
262 setproctitle = 1.0
263 setuptools = 0.6c11
264-simplejson = 2.0.9
265+simplejson = 2.1.3
266 simplesettings = 0.4
267 SimpleTal = 4.1
268 Sphinx = 1.0.7