Merge lp:~rharding/launchpad/tab_client into lp:launchpad

Proposed by Richard Harding
Status: Merged
Approved by: Richard Harding
Approved revision: no longer in the source branch.
Merged at revision: 15672
Proposed branch: lp:~rharding/launchpad/tab_client
Merge into: lp:launchpad
Diff against target: 2517 lines (+1239/-1238)
1 file modified
lib/lp/app/javascript/client.js (+1239/-1238)
To merge this branch: bzr merge lp:~rharding/launchpad/tab_client
Reviewer Review Type Date Requested Status
Benji York (community) code Approve
Review via email: mp+116329@code.launchpad.net

Commit message

Correct client.js indentation.

Description of the change

This is the first step to some linting/gardening of the JS in the client.js module. To ease diff and review this branch only tabs in the code within each YUI.add() block. This helps note that there are indeed two of those blocks here and we can now find the start/end of each.

I also did a small tweak to the end of the file to clean up the requires line.

There are no changes to the code itself, just the diff tool hates it when I do this.

To post a comment you must log in.
Revision history for this message
Benji York (benji) wrote :

This looks good.

To see the "true" diff I applied this to client.js and then diffed the
original against the patched with the -w (ignore all white space) flag.
That resulted in this:

--- original 2012-07-23 16:46:17.294237435 -0400
+++ patched 2012-07-23 16:46:10.942120030 -0400
@@ -1068,6 +1068,7 @@
  *
  * @module lp.client.plugins
  */
+ var module = Y.namespace('lp.client.plugins');

 /**
  * This plugin overrides the widget _saveData method to update the
@@ -1276,8 +1277,8 @@
     }
 });

-Y.namespace('lp.client.plugins');
-Y.lp.client.plugins.PATCHPlugin = PATCHPlugin;
+ module.PATCHPlugin = PATCHPlugin;

- }, "0.1", {"requires": [
- "plugin", "dump", "lazr.editor", "lp.client"]});
+}, "0.1", {
+ requires: ["plugin", "dump", "lazr.editor", "lp.client"]
+});

...which reveals the small, non-whitespace changes you described in the
merge proposal comment.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/app/javascript/client.js'
2--- lib/lp/app/javascript/client.js 2012-06-26 17:06:01 +0000
3+++ lib/lp/app/javascript/client.js 2012-07-23 17:10:27 +0000
4@@ -8,1052 +8,1052 @@
5 */
6 YUI.add('lp.client', function(Y) {
7
8-var module = Y.namespace('lp.client');
9-
10-module.HTTP_CREATED = 201;
11-module.HTTP_SEE_ALSO = 303;
12-module.HTTP_NOT_FOUND = 404;
13-
14-module.XHTML = 'application/xhtml+xml';
15-
16-/* Log the normal attributes accessible via o[key], and if it is a
17- * YUI node, log all of the attributes accessible via o.get(key).
18- * This function is not recursive to keep the log output reasonable.
19- *
20- * @method log_object
21- * @param o The object being logged.
22- * @param {String} name An optional name to describe the object.
23- */
24-module.log_object = function(o, name) {
25- var result;
26- var format = function(value) {
27- if (typeof value === 'string') {
28- value = value.substring(0, 200); // Truncate long strings.
29- return '"' + value + '"';
30- } else if (typeof value === 'function') {
31- // Only log the function parameters instead
32- // of the whole code block.
33- return String(value).split(" {")[0];
34- } else if (value instanceof Array) {
35- return 'Array of length ' + value.length;
36- } else {
37- return String(value);
38- }
39- };
40-
41- var introspect = function(collection) {
42- var items = [];
43- var keys = [];
44- var key;
45- var index;
46- for (key in collection) {
47- if (collection.hasOwnProperty(key)) {
48- keys.push(key);
49- }
50- }
51- keys.sort();
52- for (index in keys) {
53- if (keys.hasOwnProperty(index)) {
54- key = keys[index];
55- var value;
56- try {
57- value = format(collection[key]);
58- } catch (e) {
59- // This is necessary to handle attributes which
60- // will throw a permission denied error.
61- value = e.message;
62- }
63- items.push(key + '=' + value);
64- }
65- }
66- return items.join(',\n ');
67- };
68-
69- if (o === null || typeof o === 'string' || typeof o === 'function') {
70- result = format(o);
71- } else {
72- result = '(direct-attributes)\n ' + introspect(o);
73- if (o.getAttrs !== undefined) {
74- result += '\n(get()-attributes)\n ' + introspect(o.getAttrs());
75- }
76- }
77- if (name !== undefined) {
78- result = name + ': ' + result;
79- }
80- Y.log(result);
81-};
82-
83-// Generally useful functions.
84-/* Helper to select the io_provider. */
85-module.get_io_provider = function(io_provider) {
86- if (io_provider === undefined) {
87- return Y;
88- }
89- return io_provider;
90-};
91-
92-/* Helper to select the io_provider from a config. */
93-module.get_configured_io_provider = function(config, key) {
94- if (key === undefined) {
95- key = 'io_provider';
96- }
97- if (config === undefined || config[key] === undefined) {
98- return Y;
99- }
100- return config[key];
101-};
102-
103-module.append_qs = function(qs, key, value) {
104- /* Append a key-value pair to a query string. */
105- var elems = (qs && qs.length > 0) ? [qs] : [];
106- var enc = encodeURIComponent;
107- if (Y.Lang.isArray(value)) {
108- var index;
109- for (index = 0; index < value.length; index++) {
110- elems.push(enc(key) + "=" + enc(value[index]));
111- }
112- }
113- else {
114- elems.push(enc(key) + "=" + enc(value));
115- }
116- return elems.join("&");
117-};
118-
119-module.normalize_uri = function(uri) {
120- /* Converts an absolute URI into a relative URI.
121-
122- Appends the root to a relative URI that lacks the root.
123-
124- Does nothing to a relative URI that includes the root.*/
125- var host_start = uri.indexOf('//');
126- if (host_start !== -1) {
127- var host_end = uri.indexOf('/', host_start+2);
128- // eg. "http://www.example.com/api/devel/foo";
129- // Don't try to insert the service base into what was an
130- // absolute URL. So "http://www.example.com/foo" becomes "/foo"
131- return uri.substring(host_end, uri.length);
132- }
133-
134- var base = "/api/devel";
135- if (uri.indexOf(base.substring(1, base.length)) === 0) {
136- // eg. "api/devel/foo"
137- return '/' + uri;
138- }
139- if (uri.indexOf(base) !== 0) {
140- if (uri.indexOf('/') !== 0) {
141- // eg. "foo/bar"
142- uri = base + '/' + uri;
143- } else {
144- // eg. "/foo/bar"
145- uri = base + uri;
146- }
147- }
148- return uri;
149-};
150-
151-/**
152- * After normalizing the uri, turn it into an absolute uri.
153- * This is useful for passing in parameters to named_post and patch.
154- *
155- * @method get_absolute_uri
156- * @param {String} uri
157- * @return {String} URI.
158- */
159-module.get_absolute_uri = function(uri) {
160- var location = document.location;
161-
162- uri = module.normalize_uri(uri);
163- return location.protocol + '//' + location.host + uri;
164-};
165-
166-/**
167- * Turn an entry resource URI and a field name into a field resource URI.
168- * @method get_field_uri
169- * @param {String} base_uri
170- * @param {String} field_name
171- * @return {String} URI
172- */
173-module.get_field_uri = function(base_uri, field_name) {
174- base_uri = module.normalize_uri(base_uri);
175- field_name = escape(field_name);
176- if (base_uri.charAt(base_uri.length - 1) === '/') {
177- return base_uri + field_name;
178- } else {
179- return base_uri + '/' + field_name;
180- }
181-};
182-
183-
184-/**
185- * Get the URL of the view for an Entry
186- * @method get_view_url
187- * @param {Entry} entry
188- * @param {String} view_name
189- * @param {String} namespace
190- * @param {String} query (optional) structured query variables to use.
191- * @return {String} URL
192- */
193-module.get_view_url = function(entry, view_name, namespace, query){
194- entry_url = Y.lp.get_url_path(entry.get('web_link'));
195- querystring = Y.QueryString.stringify(query);
196- if (querystring !== '') {
197- querystring = '?' + querystring;
198- }
199- return (
200- entry_url + '/' + view_name + '/++' + namespace + '++' + querystring);
201-};
202-
203-
204-/**
205- * Get the URL of the form for a view for an Entry
206- * @method get_form_url
207- * @param {Entry} entry
208- * @param {String} view_name
209- * @return {String} URL
210- */
211-module.get_form_url = function(entry, view_name) {
212- return module.get_view_url(entry, view_name, 'form');
213-};
214-
215-
216-/**
217- * Load the model for a view.
218- *
219- * @param entry An Entry, i.e. a Lanchpad API object
220- * @param view_name The name of the view to retrieve the model for
221- * @param config An IO config.
222- * @param query (optional) The structured query variables to use.
223- */
224-module.load_model = function(entry, view_name, config, query){
225- var url = module.get_view_url(entry, view_name, 'model', query);
226- var old_on_success = config.on.success;
227- var on = config.on;
228- on.success = module.wrap_resource_on_success;
229- var y_config = {
230- on: on,
231- 'arguments': [entry.lp_client, url, old_on_success, false]
232- };
233- var io_provider = module.get_configured_io_provider(config);
234- io_provider.io(url, y_config);
235-};
236-
237-
238-module.add_accept = function(config, headers) {
239- if (headers === undefined) {
240- headers = {};
241- }
242- var accept = config.accept || 'application/json';
243- headers.Accept = accept;
244- return headers;
245-};
246-
247-module.start_and_size = function(data, start, size) {
248- /* Create a query string with values for ws.start and/or ws.size. */
249- if (start !== undefined) {
250- data = module.append_qs(data, "ws.start", start);
251- }
252- if (size !== undefined) {
253- data = module.append_qs(data, "ws.size", size);
254- }
255- return data;
256-};
257-
258-var update_cached_object = function (cache_name, cache, entry)
259-{
260- var fields_changed = [];
261- var name;
262- var html_name;
263- for (name in cache) {
264- if (cache.hasOwnProperty(name)) {
265- var old_value = cache[name];
266- var new_value = entry.get(name);
267- if (name !== 'lp_html') {
268- if (old_value !== new_value) {
269- fields_changed.push(name);
270- cache[name] = new_value;
271- var field_updated_event_name =
272- 'lp:' + cache_name + ':' + name + ':changed';
273- var new_value_html = entry.getHTML(name);
274- var event = {
275- 'name': name,
276- 'old_value': old_value,
277- 'new_value': new_value,
278- 'new_value_html': new_value_html,
279- 'entry': entry
280- };
281- Y.fire(field_updated_event_name, event);
282- }
283- }
284- else {
285- // Since we don't care here about the content, we aren't using the
286- // values here to determine if the field has changed, so we can just
287- // update the cache.
288- for (html_name in old_value) {
289- if (old_value.hasOwnProperty(html_name)) {
290- old_value[html_name] = new_value[html_name];
291- }
292- }
293- }
294- }
295- }
296-
297- if (fields_changed.length > 0) {
298- var event_name = 'lp:' + cache_name + ':changed';
299- var event_ = {
300- 'fields_changed': fields_changed,
301- 'entry': entry
302- };
303- Y.fire(event_name, event_);
304- }
305-};
306-
307-
308-module.update_cache = function(entry) {
309- if (!entry) {
310- return;
311- }
312- var original_uri = entry.lp_original_uri;
313- var full_uri = module.get_absolute_uri(original_uri);
314- var name;
315- var cached_object;
316- for (name in LP.cache) {
317- if (LP.cache.hasOwnProperty(name)) {
318- cached_object = LP.cache[name];
319- /*jslint continue:true*/
320- if (!Y.Lang.isValue(cached_object)) {
321- continue;
322- }
323- if (cached_object.self_link === full_uri) {
324- Y.log(name + ' cached object has been updated.');
325- update_cached_object(name, cached_object, entry);
326- }
327- }
328- }
329-};
330-
331-module.wrap_resource_on_success = function(ignore, response, args) {
332- var client = args[0];
333- var uri = args[1];
334- var old_on_success = args[2];
335- var update_cache = args[3];
336- var representation, wrapped;
337- if (old_on_success) {
338- var media_type = response.getResponseHeader('Content-Type');
339- if (media_type.substring(0,16) === 'application/json') {
340- representation = Y.JSON.parse(response.responseText);
341- // If the response contains a notification header, display the
342- // notifications.
343- var notifications = response.getResponseHeader(
344- 'X-Lazr-Notifications');
345- if (notifications !== null && notifications !== "") {
346- module.display_notifications(notifications);
347- }
348- if (Y.Lang.isValue(representation) &&
349- Y.Lang.isValue(representation.self_link)) {
350- uri = representation.self_link;
351- }
352- wrapped = client.wrap_resource(uri, representation);
353- var result = old_on_success(wrapped);
354- if (update_cache) {
355- module.update_cache(wrapped);
356- }
357- return result;
358- } else {
359- return old_on_success(response.responseText);
360- }
361- }
362-};
363-
364-var NOTIFICATION_INFO = {
365- 'level10': {
366- 'selector': '.debug.message',
367- 'css_class': 'debug message'
368- },
369- 'level20': {
370- 'selector': '.informational.message',
371- 'css_class': 'informational message'
372- },
373- 'level30': {
374- 'selector': '.warning.message',
375- 'css_class': 'warning message'
376- },
377- 'level40': {
378- 'selector': '.error.message',
379- 'css_class': 'error message'
380- }
381-};
382-
383-/**
384- * Display a list of notifications - error, warning, informational or debug.
385- * @param notifications An json encoded array of (level, message) tuples.
386- */
387-module.display_notifications = function (notifications) {
388- if (notifications === undefined) {
389- return;
390- }
391- if (notifications === 'null' || notifications === null
392- || notifications === "") {
393- module.remove_notifications();
394- return;
395- }
396-
397- var notifications_by_level = {
398+ var module = Y.namespace('lp.client');
399+
400+ module.HTTP_CREATED = 201;
401+ module.HTTP_SEE_ALSO = 303;
402+ module.HTTP_NOT_FOUND = 404;
403+
404+ module.XHTML = 'application/xhtml+xml';
405+
406+ /* Log the normal attributes accessible via o[key], and if it is a
407+ * YUI node, log all of the attributes accessible via o.get(key).
408+ * This function is not recursive to keep the log output reasonable.
409+ *
410+ * @method log_object
411+ * @param o The object being logged.
412+ * @param {String} name An optional name to describe the object.
413+ */
414+ module.log_object = function(o, name) {
415+ var result;
416+ var format = function(value) {
417+ if (typeof value === 'string') {
418+ value = value.substring(0, 200); // Truncate long strings.
419+ return '"' + value + '"';
420+ } else if (typeof value === 'function') {
421+ // Only log the function parameters instead
422+ // of the whole code block.
423+ return String(value).split(" {")[0];
424+ } else if (value instanceof Array) {
425+ return 'Array of length ' + value.length;
426+ } else {
427+ return String(value);
428+ }
429+ };
430+
431+ var introspect = function(collection) {
432+ var items = [];
433+ var keys = [];
434+ var key;
435+ var index;
436+ for (key in collection) {
437+ if (collection.hasOwnProperty(key)) {
438+ keys.push(key);
439+ }
440+ }
441+ keys.sort();
442+ for (index in keys) {
443+ if (keys.hasOwnProperty(index)) {
444+ key = keys[index];
445+ var value;
446+ try {
447+ value = format(collection[key]);
448+ } catch (e) {
449+ // This is necessary to handle attributes which
450+ // will throw a permission denied error.
451+ value = e.message;
452+ }
453+ items.push(key + '=' + value);
454+ }
455+ }
456+ return items.join(',\n ');
457+ };
458+
459+ if (o === null || typeof o === 'string' || typeof o === 'function') {
460+ result = format(o);
461+ } else {
462+ result = '(direct-attributes)\n ' + introspect(o);
463+ if (o.getAttrs !== undefined) {
464+ result += '\n(get()-attributes)\n ' + introspect(o.getAttrs());
465+ }
466+ }
467+ if (name !== undefined) {
468+ result = name + ': ' + result;
469+ }
470+ Y.log(result);
471+ };
472+
473+ // Generally useful functions.
474+ /* Helper to select the io_provider. */
475+ module.get_io_provider = function(io_provider) {
476+ if (io_provider === undefined) {
477+ return Y;
478+ }
479+ return io_provider;
480+ };
481+
482+ /* Helper to select the io_provider from a config. */
483+ module.get_configured_io_provider = function(config, key) {
484+ if (key === undefined) {
485+ key = 'io_provider';
486+ }
487+ if (config === undefined || config[key] === undefined) {
488+ return Y;
489+ }
490+ return config[key];
491+ };
492+
493+ module.append_qs = function(qs, key, value) {
494+ /* Append a key-value pair to a query string. */
495+ var elems = (qs && qs.length > 0) ? [qs] : [];
496+ var enc = encodeURIComponent;
497+ if (Y.Lang.isArray(value)) {
498+ var index;
499+ for (index = 0; index < value.length; index++) {
500+ elems.push(enc(key) + "=" + enc(value[index]));
501+ }
502+ }
503+ else {
504+ elems.push(enc(key) + "=" + enc(value));
505+ }
506+ return elems.join("&");
507+ };
508+
509+ module.normalize_uri = function(uri) {
510+ /* Converts an absolute URI into a relative URI.
511+
512+ Appends the root to a relative URI that lacks the root.
513+
514+ Does nothing to a relative URI that includes the root.*/
515+ var host_start = uri.indexOf('//');
516+ if (host_start !== -1) {
517+ var host_end = uri.indexOf('/', host_start+2);
518+ // eg. "http://www.example.com/api/devel/foo";
519+ // Don't try to insert the service base into what was an
520+ // absolute URL. So "http://www.example.com/foo" becomes "/foo"
521+ return uri.substring(host_end, uri.length);
522+ }
523+
524+ var base = "/api/devel";
525+ if (uri.indexOf(base.substring(1, base.length)) === 0) {
526+ // eg. "api/devel/foo"
527+ return '/' + uri;
528+ }
529+ if (uri.indexOf(base) !== 0) {
530+ if (uri.indexOf('/') !== 0) {
531+ // eg. "foo/bar"
532+ uri = base + '/' + uri;
533+ } else {
534+ // eg. "/foo/bar"
535+ uri = base + uri;
536+ }
537+ }
538+ return uri;
539+ };
540+
541+ /**
542+ * After normalizing the uri, turn it into an absolute uri.
543+ * This is useful for passing in parameters to named_post and patch.
544+ *
545+ * @method get_absolute_uri
546+ * @param {String} uri
547+ * @return {String} URI.
548+ */
549+ module.get_absolute_uri = function(uri) {
550+ var location = document.location;
551+
552+ uri = module.normalize_uri(uri);
553+ return location.protocol + '//' + location.host + uri;
554+ };
555+
556+ /**
557+ * Turn an entry resource URI and a field name into a field resource URI.
558+ * @method get_field_uri
559+ * @param {String} base_uri
560+ * @param {String} field_name
561+ * @return {String} URI
562+ */
563+ module.get_field_uri = function(base_uri, field_name) {
564+ base_uri = module.normalize_uri(base_uri);
565+ field_name = escape(field_name);
566+ if (base_uri.charAt(base_uri.length - 1) === '/') {
567+ return base_uri + field_name;
568+ } else {
569+ return base_uri + '/' + field_name;
570+ }
571+ };
572+
573+
574+ /**
575+ * Get the URL of the view for an Entry
576+ * @method get_view_url
577+ * @param {Entry} entry
578+ * @param {String} view_name
579+ * @param {String} namespace
580+ * @param {String} query (optional) structured query variables to use.
581+ * @return {String} URL
582+ */
583+ module.get_view_url = function(entry, view_name, namespace, query){
584+ entry_url = Y.lp.get_url_path(entry.get('web_link'));
585+ querystring = Y.QueryString.stringify(query);
586+ if (querystring !== '') {
587+ querystring = '?' + querystring;
588+ }
589+ return (
590+ entry_url + '/' + view_name + '/++' + namespace + '++' + querystring);
591+ };
592+
593+
594+ /**
595+ * Get the URL of the form for a view for an Entry
596+ * @method get_form_url
597+ * @param {Entry} entry
598+ * @param {String} view_name
599+ * @return {String} URL
600+ */
601+ module.get_form_url = function(entry, view_name) {
602+ return module.get_view_url(entry, view_name, 'form');
603+ };
604+
605+
606+ /**
607+ * Load the model for a view.
608+ *
609+ * @param entry An Entry, i.e. a Lanchpad API object
610+ * @param view_name The name of the view to retrieve the model for
611+ * @param config An IO config.
612+ * @param query (optional) The structured query variables to use.
613+ */
614+ module.load_model = function(entry, view_name, config, query){
615+ var url = module.get_view_url(entry, view_name, 'model', query);
616+ var old_on_success = config.on.success;
617+ var on = config.on;
618+ on.success = module.wrap_resource_on_success;
619+ var y_config = {
620+ on: on,
621+ 'arguments': [entry.lp_client, url, old_on_success, false]
622+ };
623+ var io_provider = module.get_configured_io_provider(config);
624+ io_provider.io(url, y_config);
625+ };
626+
627+
628+ module.add_accept = function(config, headers) {
629+ if (headers === undefined) {
630+ headers = {};
631+ }
632+ var accept = config.accept || 'application/json';
633+ headers.Accept = accept;
634+ return headers;
635+ };
636+
637+ module.start_and_size = function(data, start, size) {
638+ /* Create a query string with values for ws.start and/or ws.size. */
639+ if (start !== undefined) {
640+ data = module.append_qs(data, "ws.start", start);
641+ }
642+ if (size !== undefined) {
643+ data = module.append_qs(data, "ws.size", size);
644+ }
645+ return data;
646+ };
647+
648+ var update_cached_object = function (cache_name, cache, entry)
649+ {
650+ var fields_changed = [];
651+ var name;
652+ var html_name;
653+ for (name in cache) {
654+ if (cache.hasOwnProperty(name)) {
655+ var old_value = cache[name];
656+ var new_value = entry.get(name);
657+ if (name !== 'lp_html') {
658+ if (old_value !== new_value) {
659+ fields_changed.push(name);
660+ cache[name] = new_value;
661+ var field_updated_event_name =
662+ 'lp:' + cache_name + ':' + name + ':changed';
663+ var new_value_html = entry.getHTML(name);
664+ var event = {
665+ 'name': name,
666+ 'old_value': old_value,
667+ 'new_value': new_value,
668+ 'new_value_html': new_value_html,
669+ 'entry': entry
670+ };
671+ Y.fire(field_updated_event_name, event);
672+ }
673+ }
674+ else {
675+ // Since we don't care here about the content, we aren't using the
676+ // values here to determine if the field has changed, so we can just
677+ // update the cache.
678+ for (html_name in old_value) {
679+ if (old_value.hasOwnProperty(html_name)) {
680+ old_value[html_name] = new_value[html_name];
681+ }
682+ }
683+ }
684+ }
685+ }
686+
687+ if (fields_changed.length > 0) {
688+ var event_name = 'lp:' + cache_name + ':changed';
689+ var event_ = {
690+ 'fields_changed': fields_changed,
691+ 'entry': entry
692+ };
693+ Y.fire(event_name, event_);
694+ }
695+ };
696+
697+
698+ module.update_cache = function(entry) {
699+ if (!entry) {
700+ return;
701+ }
702+ var original_uri = entry.lp_original_uri;
703+ var full_uri = module.get_absolute_uri(original_uri);
704+ var name;
705+ var cached_object;
706+ for (name in LP.cache) {
707+ if (LP.cache.hasOwnProperty(name)) {
708+ cached_object = LP.cache[name];
709+ /*jslint continue:true*/
710+ if (!Y.Lang.isValue(cached_object)) {
711+ continue;
712+ }
713+ if (cached_object.self_link === full_uri) {
714+ Y.log(name + ' cached object has been updated.');
715+ update_cached_object(name, cached_object, entry);
716+ }
717+ }
718+ }
719+ };
720+
721+ module.wrap_resource_on_success = function(ignore, response, args) {
722+ var client = args[0];
723+ var uri = args[1];
724+ var old_on_success = args[2];
725+ var update_cache = args[3];
726+ var representation, wrapped;
727+ if (old_on_success) {
728+ var media_type = response.getResponseHeader('Content-Type');
729+ if (media_type.substring(0,16) === 'application/json') {
730+ representation = Y.JSON.parse(response.responseText);
731+ // If the response contains a notification header, display the
732+ // notifications.
733+ var notifications = response.getResponseHeader(
734+ 'X-Lazr-Notifications');
735+ if (notifications !== null && notifications !== "") {
736+ module.display_notifications(notifications);
737+ }
738+ if (Y.Lang.isValue(representation) &&
739+ Y.Lang.isValue(representation.self_link)) {
740+ uri = representation.self_link;
741+ }
742+ wrapped = client.wrap_resource(uri, representation);
743+ var result = old_on_success(wrapped);
744+ if (update_cache) {
745+ module.update_cache(wrapped);
746+ }
747+ return result;
748+ } else {
749+ return old_on_success(response.responseText);
750+ }
751+ }
752+ };
753+
754+ var NOTIFICATION_INFO = {
755 'level10': {
756- 'notifications': []
757+ 'selector': '.debug.message',
758+ 'css_class': 'debug message'
759 },
760 'level20': {
761- 'notifications': []
762+ 'selector': '.informational.message',
763+ 'css_class': 'informational message'
764 },
765 'level30': {
766- 'notifications': []
767+ 'selector': '.warning.message',
768+ 'css_class': 'warning message'
769 },
770 'level40': {
771- 'notifications': []
772+ 'selector': '.error.message',
773+ 'css_class': 'error message'
774 }
775 };
776
777- // Extract the notifications from the json.
778- notifications = Y.JSON.parse(notifications);
779- Y.each(notifications, function(notification, key) {
780- var level = notification[0];
781- var message = notification[1];
782- notifications_by_level['level'+level].notifications.push(message);
783- });
784+ /**
785+ * Display a list of notifications - error, warning, informational or debug.
786+ * @param notifications An json encoded array of (level, message) tuples.
787+ */
788+ module.display_notifications = function (notifications) {
789+ if (notifications === undefined) {
790+ return;
791+ }
792+ if (notifications === 'null' || notifications === null
793+ || notifications === "") {
794+ module.remove_notifications();
795+ return;
796+ }
797
798- // The place where we want to insert the notification divs.
799- var last_message = null;
800- // A mapping from the div class to notification messages.
801- Y.each(notifications_by_level, function(info, key) {
802- Y.each(info.notifications, function(notification) {
803- var css_class = NOTIFICATION_INFO[key].css_class;
804- var node = Y.Node.create("<div class='"+css_class+"'/>");
805- node.set('innerHTML', notification);
806- if (last_message === null) {
807- var div = Y.one('div#request-notifications');
808- div.insert(node);
809- } else {
810- last_message.insert(node, 'after');
811+ var notifications_by_level = {
812+ 'level10': {
813+ 'notifications': []
814+ },
815+ 'level20': {
816+ 'notifications': []
817+ },
818+ 'level30': {
819+ 'notifications': []
820+ },
821+ 'level40': {
822+ 'notifications': []
823 }
824- last_message = node;
825- });
826- });
827-};
828-
829-/**
830- * Remove any notifications that are currently displayed.
831- */
832-module.remove_notifications = function() {
833- Y.each(NOTIFICATION_INFO, function (info) {
834- var nodes = Y.all('div#request-notifications div'+info.selector);
835- nodes.each(function(node) {
836- var parent = node.get('parentNode');
837- parent.removeChild(node);
838- });
839- });
840-};
841-
842-// The resources that come together to make Launchpad.
843-
844-// A hosted file resource.
845-
846-var HostedFile = function(client, uri, content_type, contents) {
847- /* A binary file manipulable through the web service. */
848- this.lp_client = client;
849- this.uri = uri;
850- this.content_type = content_type;
851- this.contents = contents;
852- this.io_provider = client.io_provider;
853-};
854-
855-HostedFile.prototype = {
856-
857- 'lp_save' : function(config) {
858- /* Write a new version of this file back to the web service. */
859- var on = config.on;
860- var disposition = 'attachment; filename="' + this.filename + '"';
861- var hosted_file = this;
862- var args = hosted_file;
863- var y_config = {
864- method: "PUT",
865- 'on': on,
866- 'headers': {"Content-Type": hosted_file.content_type,
867- "Content-Disposition": disposition},
868- 'arguments': args,
869- 'data': hosted_file.contents,
870- 'sync': this.lp_client.sync
871 };
872- this.io_provider.io(module.normalize_uri(hosted_file.uri), y_config);
873- },
874-
875- 'lp_delete' : function(config) {
876- var on = config.on;
877- var hosted_file = this;
878- var args = hosted_file;
879- var y_config = { method: "DELETE",
880- on: on,
881- 'arguments': args,
882- sync: this.lp_client.sync
883- };
884- this.io_provider.io(hosted_file.uri, y_config);
885- }
886-};
887-
888-module.HostedFile = HostedFile;
889-
890-var Resource = function() {
891- /* The base class for objects retrieved from Launchpad's web service. */
892-};
893-Resource.prototype = {
894- 'init': function(client, representation, uri) {
895- /* Initialize a resource with its representation and URI. */
896+
897+ // Extract the notifications from the json.
898+ notifications = Y.JSON.parse(notifications);
899+ Y.each(notifications, function(notification, key) {
900+ var level = notification[0];
901+ var message = notification[1];
902+ notifications_by_level['level'+level].notifications.push(message);
903+ });
904+
905+ // The place where we want to insert the notification divs.
906+ var last_message = null;
907+ // A mapping from the div class to notification messages.
908+ Y.each(notifications_by_level, function(info, key) {
909+ Y.each(info.notifications, function(notification) {
910+ var css_class = NOTIFICATION_INFO[key].css_class;
911+ var node = Y.Node.create("<div class='"+css_class+"'/>");
912+ node.set('innerHTML', notification);
913+ if (last_message === null) {
914+ var div = Y.one('div#request-notifications');
915+ div.insert(node);
916+ } else {
917+ last_message.insert(node, 'after');
918+ }
919+ last_message = node;
920+ });
921+ });
922+ };
923+
924+ /**
925+ * Remove any notifications that are currently displayed.
926+ */
927+ module.remove_notifications = function() {
928+ Y.each(NOTIFICATION_INFO, function (info) {
929+ var nodes = Y.all('div#request-notifications div'+info.selector);
930+ nodes.each(function(node) {
931+ var parent = node.get('parentNode');
932+ parent.removeChild(node);
933+ });
934+ });
935+ };
936+
937+ // The resources that come together to make Launchpad.
938+
939+ // A hosted file resource.
940+
941+ var HostedFile = function(client, uri, content_type, contents) {
942+ /* A binary file manipulable through the web service. */
943+ this.lp_client = client;
944+ this.uri = uri;
945+ this.content_type = content_type;
946+ this.contents = contents;
947+ this.io_provider = client.io_provider;
948+ };
949+
950+ HostedFile.prototype = {
951+
952+ 'lp_save' : function(config) {
953+ /* Write a new version of this file back to the web service. */
954+ var on = config.on;
955+ var disposition = 'attachment; filename="' + this.filename + '"';
956+ var hosted_file = this;
957+ var args = hosted_file;
958+ var y_config = {
959+ method: "PUT",
960+ 'on': on,
961+ 'headers': {"Content-Type": hosted_file.content_type,
962+ "Content-Disposition": disposition},
963+ 'arguments': args,
964+ 'data': hosted_file.contents,
965+ 'sync': this.lp_client.sync
966+ };
967+ this.io_provider.io(module.normalize_uri(hosted_file.uri), y_config);
968+ },
969+
970+ 'lp_delete' : function(config) {
971+ var on = config.on;
972+ var hosted_file = this;
973+ var args = hosted_file;
974+ var y_config = { method: "DELETE",
975+ on: on,
976+ 'arguments': args,
977+ sync: this.lp_client.sync
978+ };
979+ this.io_provider.io(hosted_file.uri, y_config);
980+ }
981+ };
982+
983+ module.HostedFile = HostedFile;
984+
985+ var Resource = function() {
986+ /* The base class for objects retrieved from Launchpad's web service. */
987+ };
988+ Resource.prototype = {
989+ 'init': function(client, representation, uri) {
990+ /* Initialize a resource with its representation and URI. */
991+ this.lp_client = client;
992+ this.lp_original_uri = uri;
993+ var key;
994+ for (key in representation) {
995+ if (representation.hasOwnProperty(key)) {
996+ this[key] = representation[key];
997+ }
998+ }
999+ },
1000+
1001+ 'lookup_value': function(key) {
1002+ /* A common getter interface for Entrys and non-Entrys. */
1003+ return this[key];
1004+ },
1005+
1006+ 'follow_link': function(link_name, config) {
1007+ /* Return the object at the other end of the named link. */
1008+ var on = config.on;
1009+ var uri = this.lookup_value(link_name + '_link');
1010+ if (uri === undefined) {
1011+ uri = this.lookup_value(link_name + '_collection_link');
1012+ }
1013+ if (uri === undefined) {
1014+ throw new Error("No such link: " + link_name);
1015+ }
1016+
1017+ // If the response is 404, it means we have a hosted file that
1018+ // doesn't exist yet. If the response is 303 and goes off to another
1019+ // site, that means we have a hosted file that does exist. Either way
1020+ // we should turn the failure into a success.
1021+ var on_success = on.success;
1022+ var old_on_failure = on.failure;
1023+ on.failure = function(ignore, response, args) {
1024+ var client = args[0];
1025+ var original_url = args[1];
1026+ if (response.status === module.HTTP_NOT_FOUND ||
1027+ response.status === module.HTTP_SEE_ALSO) {
1028+ var file = new HostedFile(client, original_url);
1029+ return on_success(file);
1030+ } else if (old_on_failure !== undefined) {
1031+ return old_on_failure(ignore, response, args);
1032+ }
1033+ };
1034+ this.lp_client.get(uri, {on: on});
1035+ },
1036+
1037+ 'named_get': function(operation_name, config) {
1038+ /* Get the result of a named GET operation on this resource. */
1039+ return this.lp_client.named_get(this.lp_original_uri, operation_name,
1040+ config);
1041+ },
1042+
1043+ 'named_post': function(operation_name, config) {
1044+ /* Trigger a named POST operation on this resource. */
1045+ return this.lp_client.named_post(this.lp_original_uri, operation_name,
1046+ config);
1047+ }
1048+ };
1049+
1050+ module.Resource = Resource;
1051+
1052+
1053+ // The service root resource.
1054+ Root = function(client, representation, uri) {
1055+ /* The root of the Launchpad web service. */
1056+ this.init(client, representation, uri);
1057+ };
1058+ Root.prototype = new Resource();
1059+
1060+ module.Root = Root;
1061+
1062+
1063+ var Collection = function(client, representation, uri) {
1064+ /* A grouped collection of objets from the Launchpad web service. */
1065+ var index, entry;
1066+ this.init(client, representation, uri);
1067+ for (index = 0 ; index < this.entries.length ; index++) {
1068+ entry = this.entries[index];
1069+ this.entries[index] = new Entry(client, entry, entry.self_link);
1070+ }
1071+ };
1072+
1073+ Collection.prototype = new Resource();
1074+
1075+ Collection.prototype.lp_slice = function(on, start, size) {
1076+ /* Retrieve a subset of the collection.
1077+
1078+ :param start: Where in the collection to start serving entries.
1079+ :param size: How many entries to serve.
1080+ */
1081+ return this.lp_client.get(this.lp_original_uri,
1082+ {on: on, start: start, size: size});
1083+ };
1084+
1085+ module.Collection = Collection;
1086+
1087+ var Entry = function(client, representation, uri) {
1088+ /* A single object from the Launchpad web service. */
1089 this.lp_client = client;
1090 this.lp_original_uri = uri;
1091+ this.dirty_attributes = [];
1092+ var entry = this;
1093+
1094+ // Copy the representation keys into our own set of attributes, and add
1095+ // an attribute-change event listener for caching purposes.
1096 var key;
1097 for (key in representation) {
1098 if (representation.hasOwnProperty(key)) {
1099- this[key] = representation[key];
1100- }
1101- }
1102- },
1103-
1104- 'lookup_value': function(key) {
1105- /* A common getter interface for Entrys and non-Entrys. */
1106- return this[key];
1107- },
1108-
1109- 'follow_link': function(link_name, config) {
1110- /* Return the object at the other end of the named link. */
1111- var on = config.on;
1112- var uri = this.lookup_value(link_name + '_link');
1113- if (uri === undefined) {
1114- uri = this.lookup_value(link_name + '_collection_link');
1115- }
1116- if (uri === undefined) {
1117- throw new Error("No such link: " + link_name);
1118- }
1119-
1120- // If the response is 404, it means we have a hosted file that
1121- // doesn't exist yet. If the response is 303 and goes off to another
1122- // site, that means we have a hosted file that does exist. Either way
1123- // we should turn the failure into a success.
1124- var on_success = on.success;
1125- var old_on_failure = on.failure;
1126- on.failure = function(ignore, response, args) {
1127- var client = args[0];
1128- var original_url = args[1];
1129- if (response.status === module.HTTP_NOT_FOUND ||
1130- response.status === module.HTTP_SEE_ALSO) {
1131- var file = new HostedFile(client, original_url);
1132- return on_success(file);
1133- } else if (old_on_failure !== undefined) {
1134- return old_on_failure(ignore, response, args);
1135- }
1136- };
1137- this.lp_client.get(uri, {on: on});
1138- },
1139-
1140- 'named_get': function(operation_name, config) {
1141- /* Get the result of a named GET operation on this resource. */
1142- return this.lp_client.named_get(this.lp_original_uri, operation_name,
1143- config);
1144- },
1145-
1146- 'named_post': function(operation_name, config) {
1147- /* Trigger a named POST operation on this resource. */
1148- return this.lp_client.named_post(this.lp_original_uri, operation_name,
1149- config);
1150- }
1151-};
1152-
1153-module.Resource = Resource;
1154-
1155-
1156-// The service root resource.
1157-Root = function(client, representation, uri) {
1158- /* The root of the Launchpad web service. */
1159- this.init(client, representation, uri);
1160-};
1161-Root.prototype = new Resource();
1162-
1163-module.Root = Root;
1164-
1165-
1166-var Collection = function(client, representation, uri) {
1167- /* A grouped collection of objets from the Launchpad web service. */
1168- var index, entry;
1169- this.init(client, representation, uri);
1170- for (index = 0 ; index < this.entries.length ; index++) {
1171- entry = this.entries[index];
1172- this.entries[index] = new Entry(client, entry, entry.self_link);
1173- }
1174-};
1175-
1176-Collection.prototype = new Resource();
1177-
1178-Collection.prototype.lp_slice = function(on, start, size) {
1179- /* Retrieve a subset of the collection.
1180-
1181- :param start: Where in the collection to start serving entries.
1182- :param size: How many entries to serve.
1183- */
1184- return this.lp_client.get(this.lp_original_uri,
1185- {on: on, start: start, size: size});
1186-};
1187-
1188-module.Collection = Collection;
1189-
1190-var Entry = function(client, representation, uri) {
1191- /* A single object from the Launchpad web service. */
1192- this.lp_client = client;
1193- this.lp_original_uri = uri;
1194- this.dirty_attributes = [];
1195- var entry = this;
1196-
1197- // Copy the representation keys into our own set of attributes, and add
1198- // an attribute-change event listener for caching purposes.
1199- var key;
1200- for (key in representation) {
1201- if (representation.hasOwnProperty(key)) {
1202- this.addAttr(key, {value: representation[key]});
1203- this.on(key + "Change", this.mark_as_dirty);
1204- }
1205- }
1206-};
1207-
1208-Entry.prototype = new Resource();
1209-
1210-// Augment with Attribute so that we can listen for attribute change events.
1211-Y.augment(Entry, Y.Attribute);
1212-
1213-Entry.prototype.mark_as_dirty = function(event) {
1214- /* Respond to an event triggered by modification to an Entry's field. */
1215- if (event.newVal !== event.prevVal) {
1216- this.dirty_attributes.push(event.attrName);
1217- }
1218-};
1219-
1220-Entry.prototype.lp_save = function(config) {
1221- /* Write modifications to this entry back to the web service. */
1222- var representation = {};
1223- var entry = this;
1224- Y.each(this.dirty_attributes, function(attribute, key) {
1225- representation[attribute] = entry.get(attribute);
1226- });
1227- var headers = {};
1228- if (this.get('http_etag') !== undefined) {
1229- headers['If-Match'] = this.get('http_etag');
1230- }
1231- var uri = module.normalize_uri(this.get('self_link'));
1232- this.lp_client.patch(uri, representation, config, headers);
1233- this.dirty_attributes = [];
1234-};
1235-
1236-Entry.prototype.lookup_value = function(key) {
1237- /* A common getter interface between Entrys and non-Entrys. */
1238- return this.get(key);
1239-};
1240-
1241-Entry.prototype.getHTML = function(key) {
1242- var lp_html = this.get('lp_html');
1243- if (lp_html) {
1244- // First look for the key.
1245- var value = lp_html[key];
1246- if (value === undefined) {
1247- // now look for key_link
1248- value = lp_html[key + '_link'];
1249- }
1250- if (value !== undefined) {
1251- var result = Y.Node.create("<span/>");
1252- result.setContent(value);
1253- return result;
1254- }
1255- }
1256- return null;
1257-};
1258-
1259-module.Entry = Entry;
1260-
1261-// The Launchpad client itself.
1262-
1263-var Launchpad = function(config) {
1264- /* A client that makes HTTP requests to Launchpad's web service. */
1265- this.io_provider = module.get_configured_io_provider(config);
1266- this.sync = (config ? config.sync : false);
1267-};
1268-
1269-Launchpad.prototype = {
1270- 'get': function (uri, config) {
1271- /* Get the current state of a resource. */
1272- var on = Y.merge(config.on);
1273- var start = config.start;
1274- var size = config.size;
1275- var data = config.data;
1276- var headers = module.add_accept(config);
1277- uri = module.normalize_uri(uri);
1278- if (data === undefined) {
1279- data = "";
1280- }
1281- if (start !== undefined || size !== undefined) {
1282- data = module.start_and_size(data, start, size);
1283- }
1284-
1285- var old_on_success = on.success;
1286- var update_cache = false;
1287- on.success = module.wrap_resource_on_success;
1288- var client = this;
1289- var y_config = {
1290- on: on,
1291- 'arguments': [client, uri, old_on_success, update_cache],
1292- 'headers': headers,
1293- data: data,
1294- sync: this.sync
1295- };
1296- return this.io_provider.io(uri, y_config);
1297- },
1298-
1299- 'named_get' : function(uri, operation_name, config) {
1300- /* Retrieve the value of a named GET operation on the given URI. */
1301- var parameters = config.parameters;
1302- var data = module.append_qs("", "ws.op", operation_name);
1303- var name;
1304- for (name in parameters) {
1305- if (parameters.hasOwnProperty(name)) {
1306- data = module.append_qs(data, name, parameters[name]);
1307- }
1308- }
1309- config.data = data;
1310- return this.get(uri, config);
1311- },
1312-
1313- 'named_post' : function (uri, operation_name, config) {
1314- /* Perform a named POST operation on the given URI. */
1315- var on = Y.merge(config.on);
1316- var parameters = config.parameters;
1317- var data;
1318- var name;
1319- uri = module.normalize_uri(uri);
1320- data = module.append_qs(data, "ws.op", operation_name);
1321- for (name in parameters) {
1322- if (parameters.hasOwnProperty(name)) {
1323- data = module.append_qs(data, name, parameters[name]);
1324- }
1325- }
1326-
1327- var old_on_success = on.success;
1328-
1329- on.success = function(unknown, response, args) {
1330- if (response.status === module.HTTP_CREATED) {
1331- // A new object was created as a result of the operation.
1332- // Get that object and run the callback on it instead.
1333- var new_location = response.getResponseHeader("Location");
1334- return client.get(new_location,
1335- { on: { success: old_on_success,
1336- failure: on.failure } });
1337- }
1338- return module.wrap_resource_on_success(undefined, response, args);
1339- };
1340- var client = this;
1341- var update_cache = false;
1342- var y_config = {
1343- method: "POST",
1344- on: on,
1345- 'arguments': [client, uri, old_on_success, update_cache],
1346- data: data,
1347- sync: this.sync
1348- };
1349- this.io_provider.io(uri, y_config);
1350- },
1351-
1352- 'patch': function(uri, representation, config, headers) {
1353- var on = Y.merge(config.on);
1354- var data = Y.JSON.stringify(representation);
1355- uri = module.normalize_uri(uri);
1356-
1357- var old_on_success = on.success;
1358- var update_cache = true;
1359- on.success = module.wrap_resource_on_success;
1360- args = [this, uri, old_on_success, update_cache];
1361-
1362- var extra_headers = {
1363- "X-HTTP-Method-Override": "PATCH",
1364- "Content-Type": "application/json",
1365- "X-Content-Type-Override": "application/json"
1366- };
1367- var name;
1368- if (headers !== undefined) {
1369- for (name in headers) {
1370- if (headers.hasOwnProperty(name)) {
1371- extra_headers[name] = headers[name];
1372- }
1373- }
1374- }
1375- extra_headers = module.add_accept(config, extra_headers);
1376-
1377- var y_config = {
1378- 'method': "POST",
1379- 'on': on,
1380- 'headers': extra_headers,
1381- 'arguments': args,
1382- 'data': data,
1383- 'sync': this.sync
1384- };
1385- this.io_provider.io(uri, y_config);
1386- },
1387-
1388- 'wrap_resource': function(uri, representation) {
1389- var key;
1390- var new_representation;
1391- /* Given a representation, turn it into a subclass of Resource. */
1392- if (representation === null || representation === undefined) {
1393- return representation;
1394- }
1395- if (representation.resource_type_link === undefined) {
1396- // This is a non-entry object returned by a named operation.
1397- // It's either a list or a random JSON object.
1398- if (representation.total_size !== undefined
1399- || representation.total_size_link !== undefined) {
1400- // It's a list. Treat it as a collection;
1401- // it should be slicable.
1402+ this.addAttr(key, {value: representation[key]});
1403+ this.on(key + "Change", this.mark_as_dirty);
1404+ }
1405+ }
1406+ };
1407+
1408+ Entry.prototype = new Resource();
1409+
1410+ // Augment with Attribute so that we can listen for attribute change events.
1411+ Y.augment(Entry, Y.Attribute);
1412+
1413+ Entry.prototype.mark_as_dirty = function(event) {
1414+ /* Respond to an event triggered by modification to an Entry's field. */
1415+ if (event.newVal !== event.prevVal) {
1416+ this.dirty_attributes.push(event.attrName);
1417+ }
1418+ };
1419+
1420+ Entry.prototype.lp_save = function(config) {
1421+ /* Write modifications to this entry back to the web service. */
1422+ var representation = {};
1423+ var entry = this;
1424+ Y.each(this.dirty_attributes, function(attribute, key) {
1425+ representation[attribute] = entry.get(attribute);
1426+ });
1427+ var headers = {};
1428+ if (this.get('http_etag') !== undefined) {
1429+ headers['If-Match'] = this.get('http_etag');
1430+ }
1431+ var uri = module.normalize_uri(this.get('self_link'));
1432+ this.lp_client.patch(uri, representation, config, headers);
1433+ this.dirty_attributes = [];
1434+ };
1435+
1436+ Entry.prototype.lookup_value = function(key) {
1437+ /* A common getter interface between Entrys and non-Entrys. */
1438+ return this.get(key);
1439+ };
1440+
1441+ Entry.prototype.getHTML = function(key) {
1442+ var lp_html = this.get('lp_html');
1443+ if (lp_html) {
1444+ // First look for the key.
1445+ var value = lp_html[key];
1446+ if (value === undefined) {
1447+ // now look for key_link
1448+ value = lp_html[key + '_link'];
1449+ }
1450+ if (value !== undefined) {
1451+ var result = Y.Node.create("<span/>");
1452+ result.setContent(value);
1453+ return result;
1454+ }
1455+ }
1456+ return null;
1457+ };
1458+
1459+ module.Entry = Entry;
1460+
1461+ // The Launchpad client itself.
1462+
1463+ var Launchpad = function(config) {
1464+ /* A client that makes HTTP requests to Launchpad's web service. */
1465+ this.io_provider = module.get_configured_io_provider(config);
1466+ this.sync = (config ? config.sync : false);
1467+ };
1468+
1469+ Launchpad.prototype = {
1470+ 'get': function (uri, config) {
1471+ /* Get the current state of a resource. */
1472+ var on = Y.merge(config.on);
1473+ var start = config.start;
1474+ var size = config.size;
1475+ var data = config.data;
1476+ var headers = module.add_accept(config);
1477+ uri = module.normalize_uri(uri);
1478+ if (data === undefined) {
1479+ data = "";
1480+ }
1481+ if (start !== undefined || size !== undefined) {
1482+ data = module.start_and_size(data, start, size);
1483+ }
1484+
1485+ var old_on_success = on.success;
1486+ var update_cache = false;
1487+ on.success = module.wrap_resource_on_success;
1488+ var client = this;
1489+ var y_config = {
1490+ on: on,
1491+ 'arguments': [client, uri, old_on_success, update_cache],
1492+ 'headers': headers,
1493+ data: data,
1494+ sync: this.sync
1495+ };
1496+ return this.io_provider.io(uri, y_config);
1497+ },
1498+
1499+ 'named_get' : function(uri, operation_name, config) {
1500+ /* Retrieve the value of a named GET operation on the given URI. */
1501+ var parameters = config.parameters;
1502+ var data = module.append_qs("", "ws.op", operation_name);
1503+ var name;
1504+ for (name in parameters) {
1505+ if (parameters.hasOwnProperty(name)) {
1506+ data = module.append_qs(data, name, parameters[name]);
1507+ }
1508+ }
1509+ config.data = data;
1510+ return this.get(uri, config);
1511+ },
1512+
1513+ 'named_post' : function (uri, operation_name, config) {
1514+ /* Perform a named POST operation on the given URI. */
1515+ var on = Y.merge(config.on);
1516+ var parameters = config.parameters;
1517+ var data;
1518+ var name;
1519+ uri = module.normalize_uri(uri);
1520+ data = module.append_qs(data, "ws.op", operation_name);
1521+ for (name in parameters) {
1522+ if (parameters.hasOwnProperty(name)) {
1523+ data = module.append_qs(data, name, parameters[name]);
1524+ }
1525+ }
1526+
1527+ var old_on_success = on.success;
1528+
1529+ on.success = function(unknown, response, args) {
1530+ if (response.status === module.HTTP_CREATED) {
1531+ // A new object was created as a result of the operation.
1532+ // Get that object and run the callback on it instead.
1533+ var new_location = response.getResponseHeader("Location");
1534+ return client.get(new_location,
1535+ { on: { success: old_on_success,
1536+ failure: on.failure } });
1537+ }
1538+ return module.wrap_resource_on_success(undefined, response, args);
1539+ };
1540+ var client = this;
1541+ var update_cache = false;
1542+ var y_config = {
1543+ method: "POST",
1544+ on: on,
1545+ 'arguments': [client, uri, old_on_success, update_cache],
1546+ data: data,
1547+ sync: this.sync
1548+ };
1549+ this.io_provider.io(uri, y_config);
1550+ },
1551+
1552+ 'patch': function(uri, representation, config, headers) {
1553+ var on = Y.merge(config.on);
1554+ var data = Y.JSON.stringify(representation);
1555+ uri = module.normalize_uri(uri);
1556+
1557+ var old_on_success = on.success;
1558+ var update_cache = true;
1559+ on.success = module.wrap_resource_on_success;
1560+ args = [this, uri, old_on_success, update_cache];
1561+
1562+ var extra_headers = {
1563+ "X-HTTP-Method-Override": "PATCH",
1564+ "Content-Type": "application/json",
1565+ "X-Content-Type-Override": "application/json"
1566+ };
1567+ var name;
1568+ if (headers !== undefined) {
1569+ for (name in headers) {
1570+ if (headers.hasOwnProperty(name)) {
1571+ extra_headers[name] = headers[name];
1572+ }
1573+ }
1574+ }
1575+ extra_headers = module.add_accept(config, extra_headers);
1576+
1577+ var y_config = {
1578+ 'method': "POST",
1579+ 'on': on,
1580+ 'headers': extra_headers,
1581+ 'arguments': args,
1582+ 'data': data,
1583+ 'sync': this.sync
1584+ };
1585+ this.io_provider.io(uri, y_config);
1586+ },
1587+
1588+ 'wrap_resource': function(uri, representation) {
1589+ var key;
1590+ var new_representation;
1591+ /* Given a representation, turn it into a subclass of Resource. */
1592+ if (representation === null || representation === undefined) {
1593+ return representation;
1594+ }
1595+ if (representation.resource_type_link === undefined) {
1596+ // This is a non-entry object returned by a named operation.
1597+ // It's either a list or a random JSON object.
1598+ if (representation.total_size !== undefined
1599+ || representation.total_size_link !== undefined) {
1600+ // It's a list. Treat it as a collection;
1601+ // it should be slicable.
1602+ return new Collection(this, representation, uri);
1603+ } else if (Y.Lang.isObject(representation)) {
1604+ // It's an Array or mapping. Recurse into it.
1605+ if (Y.Lang.isArray(representation)) {
1606+ new_representation = [];
1607+ }
1608+ else {
1609+ new_representation = {};
1610+ }
1611+ for (key in representation) {
1612+ if (representation.hasOwnProperty(key)) {
1613+ var value = representation[key];
1614+ if (Y.Lang.isValue(value)) {
1615+ value = this.wrap_resource(
1616+ value.self_link, value);
1617+ }
1618+ new_representation[key] = value;
1619+ }
1620+ }
1621+ return new_representation;
1622+ } else {
1623+ // It's a random JSON object. Leave it alone.
1624+ return representation;
1625+ }
1626+ } else if (representation.resource_type_link.search(
1627+ /\/#service-root$/) !== -1) {
1628+ return new Root(this, representation, uri);
1629+ } else if (representation.total_size === undefined) {
1630+ return new Entry(this, representation, uri);
1631+ } else {
1632 return new Collection(this, representation, uri);
1633- } else if (Y.Lang.isObject(representation)) {
1634- // It's an Array or mapping. Recurse into it.
1635- if (Y.Lang.isArray(representation)) {
1636- new_representation = [];
1637- }
1638- else {
1639- new_representation = {};
1640- }
1641- for (key in representation) {
1642- if (representation.hasOwnProperty(key)) {
1643- var value = representation[key];
1644- if (Y.Lang.isValue(value)) {
1645- value = this.wrap_resource(
1646- value.self_link, value);
1647- }
1648- new_representation[key] = value;
1649+ }
1650+ }
1651+ };
1652+
1653+ module.Launchpad = Launchpad;
1654+
1655+
1656+ /**
1657+ * Helper object for handling XHR failures.
1658+ * clearProgressUI() and showError() need to be defined by the callsite
1659+ * using this object.
1660+ *
1661+ * @class ErrorHandler
1662+ */
1663+ var ErrorHandler;
1664+ ErrorHandler = function(config) {
1665+ ErrorHandler.superclass.constructor.apply(this, arguments);
1666+ };
1667+
1668+ Y.extend(ErrorHandler, Y.Base, {
1669+ /**
1670+ * Clear the progress indicator.
1671+ *
1672+ * The default implementation does nothing. Override this to provide
1673+ * an implementation to remove the UI elements used to indicate
1674+ * progress. After this method is called, the UI should be ready for
1675+ * repeating the interaction, allowing the user to retry submitting
1676+ * the data.
1677+ *
1678+ * @method clearProgressUI
1679+ */
1680+ clearProgressUI: function () {},
1681+
1682+ /**
1683+ * Show the error message to the user.
1684+ *
1685+ * The default implementation does nothing. Override this to provide
1686+ * an implementation to display the UI elements containing the error
1687+ * message.
1688+ *
1689+ * @method showError
1690+ * @param error_msg The error text to display.
1691+ */
1692+ showError: function (error_msg) {},
1693+
1694+ /**
1695+ * Handle an error from an XHR request.
1696+ *
1697+ * This method is invoked before any generic error handling is done.
1698+ *
1699+ * @method handleError
1700+ * @param ioId The request id.
1701+ * @param response The XHR call response object.
1702+ * @return {Boolean} Return true if the error has been fully processed and
1703+ * any further generic error handling is not required.
1704+ */
1705+ handleError: function(ioId, response) {
1706+ return false;
1707+ },
1708+
1709+ /**
1710+ * Return a failure handler function for XHR requests.
1711+ *
1712+ * Assign the result of this function as the failure handler when
1713+ * doing an XHR request using the API client.
1714+ *
1715+ * @method getFailureHandler
1716+ */
1717+ getFailureHandler: function () {
1718+ var self = this;
1719+ return function(ioId, o) {
1720+ self.clearProgressUI();
1721+ // Perform any user specified error handling. If true is returned,
1722+ // we do not do any further processing.
1723+ if( self.handleError(ioId, o) ) {
1724+ return;
1725+ }
1726+ // If it was a timeout...
1727+ if (o.status === 503) {
1728+ self.showError(
1729+ 'Timeout error, please try again in a few minutes.');
1730+ // If it was a server error...
1731+ } else if (o.status >= 500) {
1732+ var server_error =
1733+ 'Server error, please contact an administrator.';
1734+ var oops_id = self.get_oops_id(o);
1735+ if (oops_id) {
1736+ server_error = server_error + ' OOPS ID:' + oops_id;
1737 }
1738+ self.showError(server_error);
1739+ // Otherwise we send some sane text as an error
1740+ } else if (o.status === 412){
1741+ self.showError(o.status + ' ' + o.statusText);
1742+ } else {
1743+ self.showError(self.get_generic_error(o));
1744 }
1745- return new_representation;
1746- } else {
1747- // It's a random JSON object. Leave it alone.
1748- return representation;
1749- }
1750- } else if (representation.resource_type_link.search(
1751- /\/#service-root$/) !== -1) {
1752- return new Root(this, representation, uri);
1753- } else if (representation.total_size === undefined) {
1754- return new Entry(this, representation, uri);
1755- } else {
1756- return new Collection(this, representation, uri);
1757- }
1758- }
1759-};
1760-
1761-module.Launchpad = Launchpad;
1762-
1763-
1764-/**
1765- * Helper object for handling XHR failures.
1766- * clearProgressUI() and showError() need to be defined by the callsite
1767- * using this object.
1768- *
1769- * @class ErrorHandler
1770- */
1771-var ErrorHandler;
1772-ErrorHandler = function(config) {
1773- ErrorHandler.superclass.constructor.apply(this, arguments);
1774-};
1775-
1776-Y.extend(ErrorHandler, Y.Base, {
1777- /**
1778- * Clear the progress indicator.
1779- *
1780- * The default implementation does nothing. Override this to provide
1781- * an implementation to remove the UI elements used to indicate
1782- * progress. After this method is called, the UI should be ready for
1783- * repeating the interaction, allowing the user to retry submitting
1784- * the data.
1785- *
1786- * @method clearProgressUI
1787- */
1788- clearProgressUI: function () {},
1789-
1790- /**
1791- * Show the error message to the user.
1792- *
1793- * The default implementation does nothing. Override this to provide
1794- * an implementation to display the UI elements containing the error
1795- * message.
1796- *
1797- * @method showError
1798- * @param error_msg The error text to display.
1799- */
1800- showError: function (error_msg) {},
1801-
1802- /**
1803- * Handle an error from an XHR request.
1804- *
1805- * This method is invoked before any generic error handling is done.
1806- *
1807- * @method handleError
1808- * @param ioId The request id.
1809- * @param response The XHR call response object.
1810- * @return {Boolean} Return true if the error has been fully processed and
1811- * any further generic error handling is not required.
1812- */
1813- handleError: function(ioId, response) {
1814- return false;
1815- },
1816-
1817- /**
1818- * Return a failure handler function for XHR requests.
1819- *
1820- * Assign the result of this function as the failure handler when
1821- * doing an XHR request using the API client.
1822- *
1823- * @method getFailureHandler
1824- */
1825- getFailureHandler: function () {
1826- var self = this;
1827- return function(ioId, o) {
1828- self.clearProgressUI();
1829- // Perform any user specified error handling. If true is returned,
1830- // we do not do any further processing.
1831- if( self.handleError(ioId, o) ) {
1832+ };
1833+ },
1834+ get_oops_id: function(response) {
1835+ return response.getResponseHeader('X-Lazr-OopsId');
1836+ },
1837+ get_generic_error: function(response) {
1838+ return response.responseText;
1839+ }
1840+ });
1841+
1842+ module.ErrorHandler = ErrorHandler;
1843+
1844+ var FormErrorHandler;
1845+ FormErrorHandler = function(config) {
1846+ FormErrorHandler.superclass.constructor.apply(this, arguments);
1847+ };
1848+
1849+ FormErrorHandler.ATTRS = {
1850+ form: {
1851+ value: null
1852+ }
1853+ };
1854+
1855+ Y.extend(FormErrorHandler, ErrorHandler, {
1856+
1857+ // Clear any errors on the form.
1858+ clearFormErrors: function() {
1859+ Y.all('.error.message').remove(true);
1860+ Y.all('.error .message').remove(true);
1861+ Y.all('div.error').removeClass('error');
1862+ },
1863+
1864+ // If the XHR call returns a form validation error, we display the errors
1865+ // on the form.
1866+ handleError: function(ioId, response) {
1867+ if (response.status === 400
1868+ && response.statusText === 'Validation') {
1869+ var response_info = Y.JSON.parse(response.responseText);
1870+ var error_summary = response_info.error_summary;
1871+ var form_wide_errors = response_info.form_wide_errors;
1872+ var errors = response_info.errors;
1873+ this.handleFormValidationError(
1874+ error_summary, form_wide_errors, errors);
1875+ return true;
1876+ }
1877+ return false;
1878+ },
1879+
1880+ // Display the specified errors on the form. The errors are displayed in
1881+ // the same way as is done by the Launchpad HTML form rendering
1882+ // infrastructure using TAL templates.
1883+ handleFormValidationError: function(error_summary,
1884+ form_wide_errors, errors) {
1885+ var form = this.get('form');
1886+ if (!Y.Lang.isValue(form)) {
1887+ form = Y.one("[name='launchpadform']");
1888+ }
1889+ if (!Y.Lang.isValue(form)) {
1890 return;
1891 }
1892- // If it was a timeout...
1893- if (o.status === 503) {
1894- self.showError(
1895- 'Timeout error, please try again in a few minutes.');
1896- // If it was a server error...
1897- } else if (o.status >= 500) {
1898- var server_error =
1899- 'Server error, please contact an administrator.';
1900- var oops_id = self.get_oops_id(o);
1901- if (oops_id) {
1902- server_error = server_error + ' OOPS ID:' + oops_id;
1903+ var form_content = form.one('table.form');
1904+ if (!Y.Lang.isValue(form_content)) {
1905+ form_content = form;
1906+ }
1907+ // Display the error summary information.
1908+ var error_summary_node =
1909+ Y.Node.create('<p class="error message"></p>')
1910+ .set('text', error_summary);
1911+ form_content.insertBefore(error_summary_node, form_content);
1912+ // Display the form wide errors.
1913+ if (form_wide_errors.length > 0) {
1914+ var form_error_node =
1915+ Y.Node.create('<div class="error message"></div>');
1916+ Y.Array.each(form_wide_errors, function(message) {
1917+ form_error_node.appendChild(Y.Node.create('<p></p>')
1918+ .set('text', message));
1919+ });
1920+ form_content.insertBefore(form_error_node, form_content);
1921+ }
1922+ // Display the field specific errors.
1923+ Y.each(errors, function(message, field_name) {
1924+ var label = Y.one('label[for="' + field_name + '"]');
1925+ if (Y.Lang.isValue(label)) {
1926+ label.ancestor('div').addClass('error');
1927+ var field = label.next('div');
1928+ var error_node =
1929+ Y.Node.create('<div class="message"></div>')
1930+ .set('text', message);
1931+ field.insert(error_node, 'after');
1932 }
1933- self.showError(server_error);
1934- // Otherwise we send some sane text as an error
1935- } else if (o.status === 412){
1936- self.showError(o.status + ' ' + o.statusText);
1937- } else {
1938- self.showError(self.get_generic_error(o));
1939- }
1940- };
1941- },
1942- get_oops_id: function(response) {
1943- return response.getResponseHeader('X-Lazr-OopsId');
1944- },
1945- get_generic_error: function(response) {
1946- return response.responseText;
1947- }
1948-});
1949-
1950-module.ErrorHandler = ErrorHandler;
1951-
1952-var FormErrorHandler;
1953-FormErrorHandler = function(config) {
1954- FormErrorHandler.superclass.constructor.apply(this, arguments);
1955-};
1956-
1957-FormErrorHandler.ATTRS = {
1958- form: {
1959- value: null
1960- }
1961-};
1962-
1963-Y.extend(FormErrorHandler, ErrorHandler, {
1964-
1965- // Clear any errors on the form.
1966- clearFormErrors: function() {
1967- Y.all('.error.message').remove(true);
1968- Y.all('.error .message').remove(true);
1969- Y.all('div.error').removeClass('error');
1970- },
1971-
1972- // If the XHR call returns a form validation error, we display the errors
1973- // on the form.
1974- handleError: function(ioId, response) {
1975- if (response.status === 400
1976- && response.statusText === 'Validation') {
1977- var response_info = Y.JSON.parse(response.responseText);
1978- var error_summary = response_info.error_summary;
1979- var form_wide_errors = response_info.form_wide_errors;
1980- var errors = response_info.errors;
1981- this.handleFormValidationError(
1982- error_summary, form_wide_errors, errors);
1983- return true;
1984- }
1985- return false;
1986- },
1987-
1988- // Display the specified errors on the form. The errors are displayed in
1989- // the same way as is done by the Launchpad HTML form rendering
1990- // infrastructure using TAL templates.
1991- handleFormValidationError: function(error_summary,
1992- form_wide_errors, errors) {
1993- var form = this.get('form');
1994- if (!Y.Lang.isValue(form)) {
1995- form = Y.one("[name='launchpadform']");
1996- }
1997- if (!Y.Lang.isValue(form)) {
1998- return;
1999- }
2000- var form_content = form.one('table.form');
2001- if (!Y.Lang.isValue(form_content)) {
2002- form_content = form;
2003- }
2004- // Display the error summary information.
2005- var error_summary_node =
2006- Y.Node.create('<p class="error message"></p>')
2007- .set('text', error_summary);
2008- form_content.insertBefore(error_summary_node, form_content);
2009- // Display the form wide errors.
2010- if (form_wide_errors.length > 0) {
2011- var form_error_node =
2012- Y.Node.create('<div class="error message"></div>');
2013- Y.Array.each(form_wide_errors, function(message) {
2014- form_error_node.appendChild(Y.Node.create('<p></p>')
2015- .set('text', message));
2016 });
2017- form_content.insertBefore(form_error_node, form_content);
2018- }
2019- // Display the field specific errors.
2020- Y.each(errors, function(message, field_name) {
2021- var label = Y.one('label[for="' + field_name + '"]');
2022- if (Y.Lang.isValue(label)) {
2023- label.ancestor('div').addClass('error');
2024- var field = label.next('div');
2025- var error_node =
2026- Y.Node.create('<div class="message"></div>')
2027- .set('text', message);
2028- field.insert(error_node, 'after');
2029- }
2030- });
2031- },
2032-
2033- get_oops_id: function(response) {
2034- var oops_re = /code class\="oopsid">(OOPS-[^<]*)/;
2035- var result = response.responseText.match(oops_re);
2036- if (result === null) {
2037- return null;
2038- }
2039- return result[1];
2040- },
2041-
2042- get_generic_error: function(response) {
2043- if (response.status !== 403){
2044- return "Sorry, you don't have permission to make this change.";
2045- }
2046- else {
2047- return response.status + ' ' + response.statusText;
2048- }
2049- }
2050-});
2051-
2052-module.FormErrorHandler = FormErrorHandler;
2053+ },
2054+
2055+ get_oops_id: function(response) {
2056+ var oops_re = /code class\="oopsid">(OOPS-[^<]*)/;
2057+ var result = response.responseText.match(oops_re);
2058+ if (result === null) {
2059+ return null;
2060+ }
2061+ return result[1];
2062+ },
2063+
2064+ get_generic_error: function(response) {
2065+ if (response.status !== 403){
2066+ return "Sorry, you don't have permission to make this change.";
2067+ }
2068+ else {
2069+ return response.status + ' ' + response.statusText;
2070+ }
2071+ }
2072+ });
2073+
2074+ module.FormErrorHandler = FormErrorHandler;
2075
2076
2077 }, "0.1",
2078@@ -1063,221 +1063,222 @@
2079
2080 YUI.add('lp.client.plugins', function (Y) {
2081
2082-/**
2083- * A collection of plugins to hook lp.client into widgets.
2084- *
2085- * @module lp.client.plugins
2086- */
2087-
2088-/**
2089- * This plugin overrides the widget _saveData method to update the
2090- * underlying model object using a PATCH call.
2091- *
2092- * @namespace lp.client.plugins
2093- * @class PATCHPlugin
2094- * @extends Widget
2095- */
2096-var PATCHPlugin = function PATCHPlugin () {
2097- PATCHPlugin.superclass.constructor.apply(this, arguments);
2098-};
2099-
2100-Y.mix(PATCHPlugin, {
2101- /**
2102- * The identity of the plugin.
2103- *
2104- * @property PATCHPlugin.NAME
2105- * @type String
2106- * @static
2107- */
2108- NAME: 'PATCHPlugin',
2109-
2110- /**
2111- * The namespace of the plugin.
2112- *
2113- * @property PATCHPlugin.NS
2114- * @type String
2115- * @static
2116- */
2117- NS: 'patcher',
2118-
2119- /**
2120- * Static property used to define the default attribute configuration of
2121- * this plugin.
2122- *
2123- * @property PATCHPlugin.ATTRS
2124- * @type Object
2125- * @static
2126- */
2127- ATTRS : {
2128- /**
2129- * Name of the attribute to patch.
2130- *
2131- * @attribute patch
2132- * @type String
2133- */
2134- patch: {},
2135-
2136- /**
2137- * URL of the resource to PATCH.
2138- *
2139- * @attribute resource
2140- * @type String
2141- */
2142- resource: {},
2143-
2144- /**
2145- * Should the resulting field get the value from the lp_html
2146- * attribute?
2147- *
2148- * @attribute use_html
2149- * @type Boolean
2150- */
2151- use_html: false,
2152-
2153- /**
2154- * The function to use to format the returned result into a form that
2155- * can be inserted into the page DOM.
2156- *
2157- * The default value is a function that simply returns the result
2158- * unmodified.
2159- *
2160- * @attribute formatter
2161- * @type Function
2162- * @default null
2163- */
2164- formatter: {
2165- valueFn: function() { return this._defaultFormatter; }
2166- }
2167-}});
2168-
2169-Y.extend(PATCHPlugin, Y.Plugin.Base, {
2170-
2171- /**
2172- * Configuration parameters that will be passed through to the lp.client
2173- * call.
2174- *
2175- * @property extra_config
2176- * @type Hash
2177- */
2178- extra_config: null,
2179-
2180- /**
2181- * Constructor code. Check that the required config parameters are
2182- * present and wrap the host _saveData method.
2183- *
2184- * @method initializer
2185- * @protected
2186- */
2187- initializer: function(config) {
2188- if (!Y.Lang.isString(config.patch)) {
2189- Y.error("missing config: 'patch' containing the attribute name");
2190- }
2191-
2192- if (!Y.Lang.isString(config.resource)) {
2193- Y.error("missing config: 'resource' containing the URL to patch");
2194- }
2195-
2196- // Save the config object that the user passed in so that we can pass
2197- // any extra parameters through to the lp.client constructor.
2198- this.extra_config = config || {};
2199- this.extra_config.accept = 'application/json;include=lp_html';
2200-
2201- // Save a reference to the original _saveData()
2202- //method before wrapping it.
2203- this.original_save = config.host._saveData;
2204-
2205- // We want to run our PATCH code instead of the original
2206- // 'save' method. Using doBefore() means that
2207- // unplugging our code will leave the original
2208- // widget in a clean state.
2209- this.doBefore("_saveData", this.doPATCH);
2210-
2211- var self = this;
2212- this.error_handler = new Y.lp.client.ErrorHandler();
2213- this.error_handler.clearProgressUI = function () {
2214- config.host._uiClearWaiting();
2215- };
2216- this.error_handler.showError = function (error_msg) {
2217- config.host.showError(error_msg);
2218- };
2219- },
2220-
2221- /**
2222- * Send a PATCH request with the widget's input value for the
2223- * configured attribute.
2224- *
2225- * It will set the widget in waiting status, do the PATCH.
2226- * Success will call the original widget save method.
2227- *
2228- * Errors are reported through the widget's showError() method.
2229- *
2230- * @method doPATCH
2231- */
2232- doPATCH: function() {
2233- var owner = this.get("host"),
2234- original_save = this.original_save;
2235-
2236- // Set the widget in 'waiting' state.
2237- owner._uiSetWaiting();
2238-
2239- var client = new Y.lp.client.Launchpad();
2240- var formatter = Y.bind(this.get('formatter'), this);
2241- var attribute = this.get('patch');
2242-
2243- var patch_payload;
2244- var val = owner.getInput();
2245- patch_payload = {};
2246- patch_payload[attribute] = val;
2247-
2248- var callbacks = {
2249- on: {
2250- success: function (entry) {
2251- owner._uiClearWaiting();
2252- var new_value = formatter(entry, attribute);
2253- original_save.apply(owner, [new_value]);
2254- },
2255- failure: this.error_handler.getFailureHandler()
2256- }
2257- };
2258-
2259- var cfg = Y.merge(callbacks, this.extra_config);
2260-
2261- client.patch(this.get('resource'), patch_payload, cfg);
2262-
2263- // Prevent the method we are hooking before from running.
2264- return new Y.Do.Halt();
2265- },
2266-
2267- /**
2268- * Return the webservice Entry object attribute that is to be shown in the
2269- * page DOM.
2270- *
2271- * This function may be overridden in various ways.
2272- *
2273- * @method _defaultFormatter
2274- * @protected
2275- * @param result {Entry|String} A Launchpad webservice Entry object, or
2276- * the unmodified result string if the default Content-Type wasn't used.
2277- * @param attribute {String} The resource attribute that the PATCH request
2278- * was sent to.
2279- * @return {String|Node} A string or Node instance to be inserted into
2280- * the DOM.
2281- */
2282- _defaultFormatter: function(result, attribute) {
2283- if (Y.Lang.isString(result)) {
2284- return result;
2285- } else {
2286- if (this.get('use_html')) {
2287- return result.getHTML(attribute).get('innerHTML');
2288- } else {
2289- return result.get(attribute);
2290- }
2291- }
2292- }
2293+ /**
2294+ * A collection of plugins to hook lp.client into widgets.
2295+ *
2296+ * @module lp.client.plugins
2297+ */
2298+ var module = Y.namespace('lp.client.plugins');
2299+
2300+ /**
2301+ * This plugin overrides the widget _saveData method to update the
2302+ * underlying model object using a PATCH call.
2303+ *
2304+ * @namespace lp.client.plugins
2305+ * @class PATCHPlugin
2306+ * @extends Widget
2307+ */
2308+ var PATCHPlugin = function PATCHPlugin () {
2309+ PATCHPlugin.superclass.constructor.apply(this, arguments);
2310+ };
2311+
2312+ Y.mix(PATCHPlugin, {
2313+ /**
2314+ * The identity of the plugin.
2315+ *
2316+ * @property PATCHPlugin.NAME
2317+ * @type String
2318+ * @static
2319+ */
2320+ NAME: 'PATCHPlugin',
2321+
2322+ /**
2323+ * The namespace of the plugin.
2324+ *
2325+ * @property PATCHPlugin.NS
2326+ * @type String
2327+ * @static
2328+ */
2329+ NS: 'patcher',
2330+
2331+ /**
2332+ * Static property used to define the default attribute configuration of
2333+ * this plugin.
2334+ *
2335+ * @property PATCHPlugin.ATTRS
2336+ * @type Object
2337+ * @static
2338+ */
2339+ ATTRS : {
2340+ /**
2341+ * Name of the attribute to patch.
2342+ *
2343+ * @attribute patch
2344+ * @type String
2345+ */
2346+ patch: {},
2347+
2348+ /**
2349+ * URL of the resource to PATCH.
2350+ *
2351+ * @attribute resource
2352+ * @type String
2353+ */
2354+ resource: {},
2355+
2356+ /**
2357+ * Should the resulting field get the value from the lp_html
2358+ * attribute?
2359+ *
2360+ * @attribute use_html
2361+ * @type Boolean
2362+ */
2363+ use_html: false,
2364+
2365+ /**
2366+ * The function to use to format the returned result into a form that
2367+ * can be inserted into the page DOM.
2368+ *
2369+ * The default value is a function that simply returns the result
2370+ * unmodified.
2371+ *
2372+ * @attribute formatter
2373+ * @type Function
2374+ * @default null
2375+ */
2376+ formatter: {
2377+ valueFn: function() { return this._defaultFormatter; }
2378+ }
2379+ }});
2380+
2381+ Y.extend(PATCHPlugin, Y.Plugin.Base, {
2382+
2383+ /**
2384+ * Configuration parameters that will be passed through to the lp.client
2385+ * call.
2386+ *
2387+ * @property extra_config
2388+ * @type Hash
2389+ */
2390+ extra_config: null,
2391+
2392+ /**
2393+ * Constructor code. Check that the required config parameters are
2394+ * present and wrap the host _saveData method.
2395+ *
2396+ * @method initializer
2397+ * @protected
2398+ */
2399+ initializer: function(config) {
2400+ if (!Y.Lang.isString(config.patch)) {
2401+ Y.error("missing config: 'patch' containing the attribute name");
2402+ }
2403+
2404+ if (!Y.Lang.isString(config.resource)) {
2405+ Y.error("missing config: 'resource' containing the URL to patch");
2406+ }
2407+
2408+ // Save the config object that the user passed in so that we can pass
2409+ // any extra parameters through to the lp.client constructor.
2410+ this.extra_config = config || {};
2411+ this.extra_config.accept = 'application/json;include=lp_html';
2412+
2413+ // Save a reference to the original _saveData()
2414+ //method before wrapping it.
2415+ this.original_save = config.host._saveData;
2416+
2417+ // We want to run our PATCH code instead of the original
2418+ // 'save' method. Using doBefore() means that
2419+ // unplugging our code will leave the original
2420+ // widget in a clean state.
2421+ this.doBefore("_saveData", this.doPATCH);
2422+
2423+ var self = this;
2424+ this.error_handler = new Y.lp.client.ErrorHandler();
2425+ this.error_handler.clearProgressUI = function () {
2426+ config.host._uiClearWaiting();
2427+ };
2428+ this.error_handler.showError = function (error_msg) {
2429+ config.host.showError(error_msg);
2430+ };
2431+ },
2432+
2433+ /**
2434+ * Send a PATCH request with the widget's input value for the
2435+ * configured attribute.
2436+ *
2437+ * It will set the widget in waiting status, do the PATCH.
2438+ * Success will call the original widget save method.
2439+ *
2440+ * Errors are reported through the widget's showError() method.
2441+ *
2442+ * @method doPATCH
2443+ */
2444+ doPATCH: function() {
2445+ var owner = this.get("host"),
2446+ original_save = this.original_save;
2447+
2448+ // Set the widget in 'waiting' state.
2449+ owner._uiSetWaiting();
2450+
2451+ var client = new Y.lp.client.Launchpad();
2452+ var formatter = Y.bind(this.get('formatter'), this);
2453+ var attribute = this.get('patch');
2454+
2455+ var patch_payload;
2456+ var val = owner.getInput();
2457+ patch_payload = {};
2458+ patch_payload[attribute] = val;
2459+
2460+ var callbacks = {
2461+ on: {
2462+ success: function (entry) {
2463+ owner._uiClearWaiting();
2464+ var new_value = formatter(entry, attribute);
2465+ original_save.apply(owner, [new_value]);
2466+ },
2467+ failure: this.error_handler.getFailureHandler()
2468+ }
2469+ };
2470+
2471+ var cfg = Y.merge(callbacks, this.extra_config);
2472+
2473+ client.patch(this.get('resource'), patch_payload, cfg);
2474+
2475+ // Prevent the method we are hooking before from running.
2476+ return new Y.Do.Halt();
2477+ },
2478+
2479+ /**
2480+ * Return the webservice Entry object attribute that is to be shown in the
2481+ * page DOM.
2482+ *
2483+ * This function may be overridden in various ways.
2484+ *
2485+ * @method _defaultFormatter
2486+ * @protected
2487+ * @param result {Entry|String} A Launchpad webservice Entry object, or
2488+ * the unmodified result string if the default Content-Type wasn't used.
2489+ * @param attribute {String} The resource attribute that the PATCH request
2490+ * was sent to.
2491+ * @return {String|Node} A string or Node instance to be inserted into
2492+ * the DOM.
2493+ */
2494+ _defaultFormatter: function(result, attribute) {
2495+ if (Y.Lang.isString(result)) {
2496+ return result;
2497+ } else {
2498+ if (this.get('use_html')) {
2499+ return result.getHTML(attribute).get('innerHTML');
2500+ } else {
2501+ return result.get(attribute);
2502+ }
2503+ }
2504+ }
2505+ });
2506+
2507+ module.PATCHPlugin = PATCHPlugin;
2508+
2509+}, "0.1", {
2510+ requires: ["plugin", "dump", "lazr.editor", "lp.client"]
2511 });
2512-
2513-Y.namespace('lp.client.plugins');
2514-Y.lp.client.plugins.PATCHPlugin = PATCHPlugin;
2515-
2516- }, "0.1", {"requires": [
2517- "plugin", "dump", "lazr.editor", "lp.client"]});