launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #10175
[Merge] lp:~rharding/launchpad/lpclient_fix into lp:launchpad
Richard Harding has proposed merging lp:~rharding/launchpad/lpclient_fix into lp:launchpad with lp:~rharding/launchpad/tab_client as a prerequisite.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
Related bugs:
Bug #1004248 in Launchpad itself: "hook to force page refresh on context url changing is not working"
https://bugs.launchpad.net/launchpad/+bug/1004248
For more details, see:
https://code.launchpad.net/~rharding/launchpad/lpclient_fix/+merge/116328
TBD
--
https://code.launchpad.net/~rharding/launchpad/lpclient_fix/+merge/116328
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~rharding/launchpad/lpclient_fix into lp:launchpad.
=== modified file 'lib/lp/answers/javascript/tests/test_subscribers.js'
--- lib/lp/answers/javascript/tests/test_subscribers.js 2011-08-01 09:21:45 +0000
+++ lib/lp/answers/javascript/tests/test_subscribers.js 2012-07-23 17:08:21 +0000
@@ -62,3 +62,4 @@
Y.lp.testing.Runner.run(suite);
});
+
=== modified file 'lib/lp/app/javascript/client.js'
--- lib/lp/app/javascript/client.js 2012-06-26 17:06:01 +0000
+++ lib/lp/app/javascript/client.js 2012-07-23 17:08:21 +0000
@@ -7,1277 +7,1249 @@
* @module Y.lp.client
*/
YUI.add('lp.client', function(Y) {
-
-var module = Y.namespace('lp.client');
-
-module.HTTP_CREATED = 201;
-module.HTTP_SEE_ALSO = 303;
-module.HTTP_NOT_FOUND = 404;
-
-module.XHTML = 'application/xhtml+xml';
-
-/* Log the normal attributes accessible via o[key], and if it is a
- * YUI node, log all of the attributes accessible via o.get(key).
- * This function is not recursive to keep the log output reasonable.
- *
- * @method log_object
- * @param o The object being logged.
- * @param {String} name An optional name to describe the object.
- */
-module.log_object = function(o, name) {
- var result;
- var format = function(value) {
- if (typeof value === 'string') {
- value = value.substring(0, 200); // Truncate long strings.
- return '"' + value + '"';
- } else if (typeof value === 'function') {
- // Only log the function parameters instead
- // of the whole code block.
- return String(value).split(" {")[0];
- } else if (value instanceof Array) {
- return 'Array of length ' + value.length;
- } else {
- return String(value);
- }
- };
-
- var introspect = function(collection) {
- var items = [];
- var keys = [];
- var key;
- var index;
- for (key in collection) {
- if (collection.hasOwnProperty(key)) {
- keys.push(key);
- }
- }
- keys.sort();
- for (index in keys) {
- if (keys.hasOwnProperty(index)) {
- key = keys[index];
- var value;
- try {
- value = format(collection[key]);
- } catch (e) {
- // This is necessary to handle attributes which
- // will throw a permission denied error.
- value = e.message;
- }
- items.push(key + '=' + value);
- }
- }
- return items.join(',\n ');
- };
-
- if (o === null || typeof o === 'string' || typeof o === 'function') {
- result = format(o);
- } else {
- result = '(direct-attributes)\n ' + introspect(o);
- if (o.getAttrs !== undefined) {
- result += '\n(get()-attributes)\n ' + introspect(o.getAttrs());
- }
- }
- if (name !== undefined) {
- result = name + ': ' + result;
- }
- Y.log(result);
-};
-
-// Generally useful functions.
-/* Helper to select the io_provider. */
-module.get_io_provider = function(io_provider) {
- if (io_provider === undefined) {
- return Y;
- }
- return io_provider;
-};
-
-/* Helper to select the io_provider from a config. */
-module.get_configured_io_provider = function(config, key) {
- if (key === undefined) {
- key = 'io_provider';
- }
- if (config === undefined || config[key] === undefined) {
- return Y;
- }
- return config[key];
-};
-
-module.append_qs = function(qs, key, value) {
- /* Append a key-value pair to a query string. */
- var elems = (qs && qs.length > 0) ? [qs] : [];
- var enc = encodeURIComponent;
- if (Y.Lang.isArray(value)) {
- var index;
- for (index = 0; index < value.length; index++) {
- elems.push(enc(key) + "=" + enc(value[index]));
- }
- }
- else {
- elems.push(enc(key) + "=" + enc(value));
- }
- return elems.join("&");
-};
-
-module.normalize_uri = function(uri) {
- /* Converts an absolute URI into a relative URI.
-
- Appends the root to a relative URI that lacks the root.
-
- Does nothing to a relative URI that includes the root.*/
- var host_start = uri.indexOf('//');
- if (host_start !== -1) {
- var host_end = uri.indexOf('/', host_start+2);
- // eg. "http://www.example.com/api/devel/foo";
- // Don't try to insert the service base into what was an
- // absolute URL. So "http://www.example.com/foo" becomes "/foo"
- return uri.substring(host_end, uri.length);
- }
-
- var base = "/api/devel";
- if (uri.indexOf(base.substring(1, base.length)) === 0) {
- // eg. "api/devel/foo"
- return '/' + uri;
- }
- if (uri.indexOf(base) !== 0) {
- if (uri.indexOf('/') !== 0) {
- // eg. "foo/bar"
- uri = base + '/' + uri;
- } else {
- // eg. "/foo/bar"
- uri = base + uri;
- }
- }
- return uri;
-};
-
-/**
- * After normalizing the uri, turn it into an absolute uri.
- * This is useful for passing in parameters to named_post and patch.
- *
- * @method get_absolute_uri
- * @param {String} uri
- * @return {String} URI.
- */
-module.get_absolute_uri = function(uri) {
- var location = document.location;
-
- uri = module.normalize_uri(uri);
- return location.protocol + '//' + location.host + uri;
-};
-
-/**
- * Turn an entry resource URI and a field name into a field resource URI.
- * @method get_field_uri
- * @param {String} base_uri
- * @param {String} field_name
- * @return {String} URI
- */
-module.get_field_uri = function(base_uri, field_name) {
- base_uri = module.normalize_uri(base_uri);
- field_name = escape(field_name);
- if (base_uri.charAt(base_uri.length - 1) === '/') {
- return base_uri + field_name;
- } else {
- return base_uri + '/' + field_name;
- }
-};
-
-
-/**
- * Get the URL of the view for an Entry
- * @method get_view_url
- * @param {Entry} entry
- * @param {String} view_name
- * @param {String} namespace
- * @param {String} query (optional) structured query variables to use.
- * @return {String} URL
- */
-module.get_view_url = function(entry, view_name, namespace, query){
- entry_url = Y.lp.get_url_path(entry.get('web_link'));
- querystring = Y.QueryString.stringify(query);
- if (querystring !== '') {
- querystring = '?' + querystring;
- }
- return (
- entry_url + '/' + view_name + '/++' + namespace + '++' + querystring);
-};
-
-
-/**
- * Get the URL of the form for a view for an Entry
- * @method get_form_url
- * @param {Entry} entry
- * @param {String} view_name
- * @return {String} URL
- */
-module.get_form_url = function(entry, view_name) {
- return module.get_view_url(entry, view_name, 'form');
-};
-
-
-/**
- * Load the model for a view.
- *
- * @param entry An Entry, i.e. a Lanchpad API object
- * @param view_name The name of the view to retrieve the model for
- * @param config An IO config.
- * @param query (optional) The structured query variables to use.
- */
-module.load_model = function(entry, view_name, config, query){
- var url = module.get_view_url(entry, view_name, 'model', query);
- var old_on_success = config.on.success;
- var on = config.on;
- on.success = module.wrap_resource_on_success;
- var y_config = {
- on: on,
- 'arguments': [entry.lp_client, url, old_on_success, false]
- };
- var io_provider = module.get_configured_io_provider(config);
- io_provider.io(url, y_config);
-};
-
-
-module.add_accept = function(config, headers) {
- if (headers === undefined) {
- headers = {};
- }
- var accept = config.accept || 'application/json';
- headers.Accept = accept;
- return headers;
-};
-
-module.start_and_size = function(data, start, size) {
- /* Create a query string with values for ws.start and/or ws.size. */
- if (start !== undefined) {
- data = module.append_qs(data, "ws.start", start);
- }
- if (size !== undefined) {
- data = module.append_qs(data, "ws.size", size);
- }
- return data;
-};
-
-var update_cached_object = function (cache_name, cache, entry)
-{
- var fields_changed = [];
- var name;
- var html_name;
- for (name in cache) {
- if (cache.hasOwnProperty(name)) {
- var old_value = cache[name];
- var new_value = entry.get(name);
- if (name !== 'lp_html') {
- if (old_value !== new_value) {
- fields_changed.push(name);
- cache[name] = new_value;
- var field_updated_event_name =
- 'lp:' + cache_name + ':' + name + ':changed';
- var new_value_html = entry.getHTML(name);
- var event = {
- 'name': name,
- 'old_value': old_value,
- 'new_value': new_value,
- 'new_value_html': new_value_html,
- 'entry': entry
- };
- Y.fire(field_updated_event_name, event);
- }
- }
- else {
- // Since we don't care here about the content, we aren't using the
- // values here to determine if the field has changed, so we can just
- // update the cache.
- for (html_name in old_value) {
- if (old_value.hasOwnProperty(html_name)) {
- old_value[html_name] = new_value[html_name];
- }
- }
- }
- }
- }
-
- if (fields_changed.length > 0) {
- var event_name = 'lp:' + cache_name + ':changed';
- var event_ = {
- 'fields_changed': fields_changed,
- 'entry': entry
- };
- Y.fire(event_name, event_);
- }
-};
-
-
-module.update_cache = function(entry) {
- if (!entry) {
- return;
- }
- var original_uri = entry.lp_original_uri;
- var full_uri = module.get_absolute_uri(original_uri);
- var name;
- var cached_object;
- for (name in LP.cache) {
- if (LP.cache.hasOwnProperty(name)) {
- cached_object = LP.cache[name];
- /*jslint continue:true*/
- if (!Y.Lang.isValue(cached_object)) {
- continue;
- }
- if (cached_object.self_link === full_uri) {
- Y.log(name + ' cached object has been updated.');
- update_cached_object(name, cached_object, entry);
- }
- }
- }
-};
-
-module.wrap_resource_on_success = function(ignore, response, args) {
- var client = args[0];
- var uri = args[1];
- var old_on_success = args[2];
- var update_cache = args[3];
- var representation, wrapped;
- if (old_on_success) {
- var media_type = response.getResponseHeader('Content-Type');
- if (media_type.substring(0,16) === 'application/json') {
- representation = Y.JSON.parse(response.responseText);
- // If the response contains a notification header, display the
- // notifications.
- var notifications = response.getResponseHeader(
- 'X-Lazr-Notifications');
- if (notifications !== null && notifications !== "") {
- module.display_notifications(notifications);
- }
- if (Y.Lang.isValue(representation) &&
- Y.Lang.isValue(representation.self_link)) {
- uri = representation.self_link;
- }
- wrapped = client.wrap_resource(uri, representation);
- var result = old_on_success(wrapped);
- if (update_cache) {
- module.update_cache(wrapped);
- }
- return result;
- } else {
- return old_on_success(response.responseText);
- }
- }
-};
-
-var NOTIFICATION_INFO = {
- 'level10': {
- 'selector': '.debug.message',
- 'css_class': 'debug message'
- },
- 'level20': {
- 'selector': '.informational.message',
- 'css_class': 'informational message'
- },
- 'level30': {
- 'selector': '.warning.message',
- 'css_class': 'warning message'
- },
- 'level40': {
- 'selector': '.error.message',
- 'css_class': 'error message'
- }
-};
-
-/**
- * Display a list of notifications - error, warning, informational or debug.
- * @param notifications An json encoded array of (level, message) tuples.
- */
-module.display_notifications = function (notifications) {
- if (notifications === undefined) {
- return;
- }
- if (notifications === 'null' || notifications === null
- || notifications === "") {
- module.remove_notifications();
- return;
- }
-
- var notifications_by_level = {
+ // Private methods in the module.
+ var NOTIFICATION_INFO = {
'level10': {
- 'notifications': []
+ 'selector': '.debug.message',
+ 'css_class': 'debug message'
},
'level20': {
- 'notifications': []
+ 'selector': '.informational.message',
+ 'css_class': 'informational message'
},
'level30': {
- 'notifications': []
+ 'selector': '.warning.message',
+ 'css_class': 'warning message'
},
'level40': {
- 'notifications': []
- }
- };
-
- // Extract the notifications from the json.
- notifications = Y.JSON.parse(notifications);
- Y.each(notifications, function(notification, key) {
- var level = notification[0];
- var message = notification[1];
- notifications_by_level['level'+level].notifications.push(message);
- });
-
- // The place where we want to insert the notification divs.
- var last_message = null;
- // A mapping from the div class to notification messages.
- Y.each(notifications_by_level, function(info, key) {
- Y.each(info.notifications, function(notification) {
- var css_class = NOTIFICATION_INFO[key].css_class;
- var node = Y.Node.create("<div class='"+css_class+"'/>");
- node.set('innerHTML', notification);
- if (last_message === null) {
- var div = Y.one('div#request-notifications');
- div.insert(node);
- } else {
- last_message.insert(node, 'after');
- }
- last_message = node;
- });
- });
-};
-
-/**
- * Remove any notifications that are currently displayed.
- */
-module.remove_notifications = function() {
- Y.each(NOTIFICATION_INFO, function (info) {
- var nodes = Y.all('div#request-notifications div'+info.selector);
- nodes.each(function(node) {
- var parent = node.get('parentNode');
- parent.removeChild(node);
- });
- });
-};
-
-// The resources that come together to make Launchpad.
-
-// A hosted file resource.
-
-var HostedFile = function(client, uri, content_type, contents) {
- /* A binary file manipulable through the web service. */
- this.lp_client = client;
- this.uri = uri;
- this.content_type = content_type;
- this.contents = contents;
- this.io_provider = client.io_provider;
-};
-
-HostedFile.prototype = {
-
- 'lp_save' : function(config) {
- /* Write a new version of this file back to the web service. */
+ 'selector': '.error.message',
+ 'css_class': 'error message'
+ }
+ };
+
+ var update_cached_object = function (cache_name, cache, entry) {
+ var fields_changed = [];
+ var name;
+ var html_name;
+ for (name in cache) {
+ if (cache.hasOwnProperty(name)) {
+ var old_value = cache[name];
+ var new_value = entry.get(name);
+ if (name !== 'lp_html') {
+ if (old_value !== new_value) {
+ fields_changed.push(name);
+ cache[name] = new_value;
+ var field_updated_event_name =
+ 'lp:' + cache_name + ':' + name + ':changed';
+ var new_value_html = entry.getHTML(name);
+ var event = {
+ name: name,
+ old_value: old_value,
+ new_value: new_value,
+ new_value_html: new_value_html,
+ entry: entry
+ };
+ Y.fire(field_updated_event_name, event);
+ }
+ }
+ else {
+ // Since we don't care here about the content, we aren't using
+ // the values here to determine if the field has changed, so
+ // we can just update the cache.
+ for (html_name in old_value) {
+ if (old_value.hasOwnProperty(html_name)) {
+ old_value[html_name] = new_value[html_name];
+ }
+ }
+ }
+ }
+ }
+
+ if (fields_changed.length > 0) {
+ var event_name = 'lp:' + cache_name + ':changed';
+ var event_ = {
+ fields_changed: fields_changed,
+ entry: entry
+ };
+ Y.fire(event_name, event_);
+ }
+ };
+
+ var module = Y.namespace('lp.client');
+
+ module.HTTP_CREATED = 201;
+ module.HTTP_SEE_ALSO = 303;
+ module.HTTP_NOT_FOUND = 404;
+
+ module.XHTML = 'application/xhtml+xml';
+ module.GET = 'get';
+ module.POST = 'post';
+ module.PATCH = 'patch';
+
+ /* Log the normal attributes accessible via o[key], and if it is a
+ * YUI node, log all of the attributes accessible via o.get(key).
+ * This function is not recursive to keep the log output reasonable.
+ *
+ * @method log_object
+ * @param o The object being logged.
+ * @param {String} name An optional name to describe the object.
+ */
+ module.log_object = function(o, name) {
+ var result;
+ var format = function(value) {
+ if (typeof value === 'string') {
+ value = value.substring(0, 200); // Truncate long strings.
+ return '"' + value + '"';
+ } else if (typeof value === 'function') {
+ // Only log the function parameters instead
+ // of the whole code block.
+ return String(value).split(" {")[0];
+ } else if (value instanceof Array) {
+ return 'Array of length ' + value.length;
+ } else {
+ return String(value);
+ }
+ };
+
+ var introspect = function(collection) {
+ var items = [];
+ var keys = [];
+ var key;
+ var index;
+ for (key in collection) {
+ if (collection.hasOwnProperty(key)) {
+ keys.push(key);
+ }
+ }
+ keys.sort();
+ for (index in keys) {
+ if (keys.hasOwnProperty(index)) {
+ key = keys[index];
+ var value;
+ try {
+ value = format(collection[key]);
+ } catch (e) {
+ // This is necessary to handle attributes which
+ // will throw a permission denied error.
+ value = e.message;
+ }
+ items.push(key + '=' + value);
+ }
+ }
+ return items.join(',\n ');
+ };
+
+ if (o === null || typeof o === 'string' || typeof o === 'function') {
+ result = format(o);
+ } else {
+ result = '(direct-attributes)\n ' + introspect(o);
+ if (o.getAttrs !== undefined) {
+ result += '\n(get()-attributes)\n ' + introspect(o.getAttrs());
+ }
+ }
+ if (name !== undefined) {
+ result = name + ': ' + result;
+ }
+ Y.log(result);
+ };
+
+ // Generally useful functions.
+ /* Helper to select the io_provider. */
+ module.get_io_provider = function(io_provider) {
+ if (io_provider === undefined) {
+ return Y;
+ }
+ return io_provider;
+ };
+
+ /* Helper to select the io_provider from a config. */
+ module.get_configured_io_provider = function(config, key) {
+ if (key === undefined) {
+ key = 'io_provider';
+ }
+ if (config === undefined || config[key] === undefined) {
+ return Y;
+ }
+ return config[key];
+ };
+
+ module.append_qs = function(qs, key, value) {
+ /* Append a key-value pair to a query string. */
+ var elems = (qs && qs.length > 0) ? [qs] : [];
+ var enc = encodeURIComponent;
+ if (Y.Lang.isArray(value)) {
+ var index;
+ for (index = 0; index < value.length; index++) {
+ elems.push(enc(key) + "=" + enc(value[index]));
+ }
+ }
+ else {
+ elems.push(enc(key) + "=" + enc(value));
+ }
+ return elems.join("&");
+ };
+
+ module.normalize_uri = function(uri) {
+ /* Converts an absolute URI into a relative URI.
+
+ Appends the root to a relative URI that lacks the root.
+
+ Does nothing to a relative URI that includes the root.*/
+ var host_start = uri.indexOf('//');
+ if (host_start !== -1) {
+ var host_end = uri.indexOf('/', host_start+2);
+ // eg. "http://www.example.com/api/devel/foo";
+ // Don't try to insert the service base into what was an
+ // absolute URL. So "http://www.example.com/foo" becomes "/foo"
+ return uri.substring(host_end, uri.length);
+ }
+
+ var base = "/api/devel";
+ if (uri.indexOf(base.substring(1, base.length)) === 0) {
+ // eg. "api/devel/foo"
+ return '/' + uri;
+ }
+ if (uri.indexOf(base) !== 0) {
+ if (uri.indexOf('/') !== 0) {
+ // eg. "foo/bar"
+ uri = base + '/' + uri;
+ } else {
+ // eg. "/foo/bar"
+ uri = base + uri;
+ }
+ }
+ return uri;
+ };
+
+ /**
+ * After normalizing the uri, turn it into an absolute uri.
+ * This is useful for passing in parameters to named_post and patch.
+ *
+ * @method get_absolute_uri
+ * @param {String} uri
+ * @return {String} URI.
+ */
+ module.get_absolute_uri = function(uri) {
+ var location = document.location;
+
+ uri = module.normalize_uri(uri);
+ return location.protocol + '//' + location.host + uri;
+ };
+
+ /**
+ * Turn an entry resource URI and a field name into a field resource URI.
+ * @method get_field_uri
+ * @param {String} base_uri
+ * @param {String} field_name
+ * @return {String} URI
+ */
+ module.get_field_uri = function(base_uri, field_name) {
+ base_uri = module.normalize_uri(base_uri);
+ field_name = escape(field_name);
+ if (base_uri.charAt(base_uri.length - 1) === '/') {
+ return base_uri + field_name;
+ } else {
+ return base_uri + '/' + field_name;
+ }
+ };
+
+ /**
+ * Get the URL of the view for an Entry
+ * @method get_view_url
+ * @param {Entry} entry
+ * @param {String} view_name
+ * @param {String} namespace
+ * @param {String} query (optional) structured query variables to use.
+ * @return {String} URL
+ */
+ module.get_view_url = function(entry, view_name, namespace, query){
+ entry_url = Y.lp.get_url_path(entry.get('web_link'));
+ var querystring = Y.QueryString.stringify(query);
+ if (querystring !== '') {
+ querystring = '?' + querystring;
+ }
+ return (
+ entry_url + '/' + view_name + '/++' + namespace + '++' + querystring);
+ };
+
+ /**
+ * Get the URL of the form for a view for an Entry
+ * @method get_form_url
+ * @param {Entry} entry
+ * @param {String} view_name
+ * @return {String} URL
+ */
+ module.get_form_url = function(entry, view_name) {
+ return module.get_view_url(entry, view_name, 'form');
+ };
+
+ /**
+ * Load the model for a view.
+ *
+ * @param entry An Entry, i.e. a Lanchpad API object
+ * @param view_name The name of the view to retrieve the model for
+ * @param config An IO config.
+ * @param query (optional) The structured query variables to use.
+ */
+ module.load_model = function(entry, view_name, config, query){
+ var url = module.get_view_url(entry, view_name, 'model', query);
+ var old_on_success = config.on.success;
var on = config.on;
- var disposition = 'attachment; filename="' + this.filename + '"';
- var hosted_file = this;
- var args = hosted_file;
+ on.success = module.wrap_resource_on_success;
var y_config = {
- method: "PUT",
- 'on': on,
- 'headers': {"Content-Type": hosted_file.content_type,
- "Content-Disposition": disposition},
- 'arguments': args,
- 'data': hosted_file.contents,
- 'sync': this.lp_client.sync
- };
- this.io_provider.io(module.normalize_uri(hosted_file.uri), y_config);
- },
-
- 'lp_delete' : function(config) {
- var on = config.on;
- var hosted_file = this;
- var args = hosted_file;
- var y_config = { method: "DELETE",
- on: on,
- 'arguments': args,
- sync: this.lp_client.sync
- };
- this.io_provider.io(hosted_file.uri, y_config);
- }
-};
-
-module.HostedFile = HostedFile;
-
-var Resource = function() {
- /* The base class for objects retrieved from Launchpad's web service. */
-};
-Resource.prototype = {
- 'init': function(client, representation, uri) {
- /* Initialize a resource with its representation and URI. */
- this.lp_client = client;
- this.lp_original_uri = uri;
+ on: on,
+ 'arguments': [entry.lp_client, url, old_on_success, false]
+ };
+ var io_provider = module.get_configured_io_provider(config);
+ io_provider.io(url, y_config);
+ };
+
+ module.add_accept = function(config, headers) {
+ if (headers === undefined) {
+ headers = {};
+ }
+ var accept = config.accept || 'application/json';
+ headers.Accept = accept;
+ return headers;
+ };
+
+ module.start_and_size = function(data, start, size) {
+ /* Create a query string with values for ws.start and/or ws.size. */
+ if (start !== undefined) {
+ data = module.append_qs(data, "ws.start", start);
+ }
+ if (size !== undefined) {
+ data = module.append_qs(data, "ws.size", size);
+ }
+ return data;
+ };
+
+ module.update_cache = function(entry) {
+ if (!entry) {
+ return;
+ }
+ var original_uri = entry.uri;
+ var full_uri = module.get_absolute_uri(original_uri);
+
+ var name;
+ var cached_object;
+ for (name in LP.cache) {
+ if (LP.cache.hasOwnProperty(name)) {
+ cached_object = LP.cache[name];
+ /*jslint continue:true*/
+ if (!Y.Lang.isValue(cached_object)) {
+ continue;
+ }
+ if (cached_object.self_link === full_uri) {
+ Y.log(name + ' cached object has been updated.');
+ update_cached_object(name, cached_object, entry);
+ }
+ }
+ }
+ };
+
+ module.wrap_resource_on_success = function(ignore, response, args) {
+ var client = args[0];
+ // The original uri of the caller.
+ var uri = args[1];
+ var callback = args[2];
+ var update_cache = args[3];
+ var method = args[4];
+ var representation, wrapped;
+
+ if (callback) {
+ var media_type = response.getResponseHeader('Content-Type');
+ if (media_type.substring(0,16) === 'application/json') {
+ representation = Y.JSON.parse(response.responseText);
+
+ // If the object fetched has a self_link, make that the object's
+ // uri for use in other api methods off of that object.
+ // During a PATCH request the caller is the object. Leave the
+ // original_uri alone. Otherwise make the uri the object
+ // coming back.
+ if (Y.Lang.isValue(representation) &&
+ Y.Lang.isValue(representation.self_link &&
+ method !== module.PATCH)) {
+ uri = representation.self_link;
+ }
+
+ // If the response contains a notification header, display the
+ // notifications.
+ var notifications = response.getResponseHeader(
+ 'X-Lazr-Notifications');
+ if (notifications !== null && notifications !== "") {
+ module.display_notifications(notifications);
+ }
+ wrapped = client.wrap_resource(uri, representation);
+ var result = callback(wrapped);
+ if (update_cache) {
+ module.update_cache(wrapped);
+ }
+ return result;
+ } else {
+ return callback(response.responseText);
+ }
+ }
+ };
+
+ /**
+ * Display a list of notifications - error, warning, informational or debug.
+ * @param notifications An json encoded array of (level, message) tuples.
+ */
+ module.display_notifications = function (notifications) {
+ if (notifications === undefined) {
+ return;
+ }
+ if (notifications === 'null' || notifications === null
+ || notifications === "") {
+ module.remove_notifications();
+ return;
+ }
+
+ var notifications_by_level = {
+ 'level10': {
+ 'notifications': []
+ },
+ 'level20': {
+ 'notifications': []
+ },
+ 'level30': {
+ 'notifications': []
+ },
+ 'level40': {
+ 'notifications': []
+ }
+ };
+
+ // Extract the notifications from the json.
+ notifications = Y.JSON.parse(notifications);
+ Y.each(notifications, function(notification, key) {
+ var level = notification[0];
+ var message = notification[1];
+ notifications_by_level['level'+level].notifications.push(message);
+ });
+
+ // The place where we want to insert the notification divs.
+ var last_message = null;
+ // A mapping from the div class to notification messages.
+ Y.each(notifications_by_level, function(info, key) {
+ Y.each(info.notifications, function(notification) {
+ var css_class = NOTIFICATION_INFO[key].css_class;
+ var node = Y.Node.create("<div class='"+css_class+"'/>");
+ node.set('innerHTML', notification);
+ if (last_message === null) {
+ var div = Y.one('div#request-notifications');
+ div.insert(node);
+ } else {
+ last_message.insert(node, 'after');
+ }
+ last_message = node;
+ });
+ });
+ };
+
+ /**
+ * Remove any notifications that are currently displayed.
+ */
+ module.remove_notifications = function() {
+ Y.each(NOTIFICATION_INFO, function (info) {
+ var nodes = Y.all('div#request-notifications div'+info.selector);
+ nodes.each(function(node) {
+ var parent = node.get('parentNode');
+ parent.removeChild(node);
+ });
+ });
+ };
+
+ // The resources that come together to make Launchpad.
+
+ // A hosted file resource.
+ module.HostedFile = function(client, uri, content_type, contents) {
+ /* A binary file manipulable through the web service. */
+ this.lp_client = client;
+ this.uri = uri;
+ this.content_type = content_type;
+ this.contents = contents;
+ this.io_provider = client.io_provider;
+ };
+
+ module.HostedFile.prototype = {
+ 'lp_save' : function(config) {
+ /* Write a new version of this file back to the web service. */
+ var on = config.on;
+ var disposition = 'attachment; filename="' + this.filename + '"';
+ var hosted_file = this;
+ var args = hosted_file;
+ var y_config = {
+ method: "PUT",
+ 'on': on,
+ 'headers': {"Content-Type": hosted_file.content_type,
+ "Content-Disposition": disposition},
+ 'arguments': args,
+ 'data': hosted_file.contents,
+ 'sync': this.lp_client.sync
+ };
+ this.io_provider.io(module.normalize_uri(hosted_file.uri), y_config);
+ },
+
+ 'lp_delete' : function(config) {
+ var on = config.on;
+ var hosted_file = this;
+ var args = hosted_file;
+ var y_config = { method: "DELETE",
+ on: on,
+ 'arguments': args,
+ sync: this.lp_client.sync
+ };
+ this.io_provider.io(hosted_file.uri, y_config);
+ }
+ };
+
+ module.Resource = function() {
+ /* The base class for objects retrieved from Launchpad's web service. */
+ };
+ module.Resource.prototype = {
+ 'init': function(client, representation, uri) {
+ /* Initialize a resource with its representation and URI. */
+ this.lp_client = client;
+ this.uri = uri;
+ var key;
+ for (key in representation) {
+ if (representation.hasOwnProperty(key)) {
+ this[key] = representation[key];
+ }
+ }
+ },
+
+ 'lookup_value': function(key) {
+ /* A common getter interface for Entrys and non-Entrys. */
+ return this[key];
+ },
+
+ 'follow_link': function(link_name, config) {
+ /* Return the object at the other end of the named link. */
+ var on = config.on;
+ var uri = this.lookup_value(link_name + '_link');
+ if (uri === undefined) {
+ uri = this.lookup_value(link_name + '_collection_link');
+ }
+ if (uri === undefined) {
+ throw new Error("No such link: " + link_name);
+ }
+
+ // If the response is 404, it means we have a hosted file that
+ // doesn't exist yet. If the response is 303 and goes off to another
+ // site, that means we have a hosted file that does exist. Either way
+ // we should turn the failure into a success.
+ var on_success = on.success;
+ var old_on_failure = on.failure;
+ on.failure = function(ignore, response, args) {
+ var client = args[0];
+ var original_url = args[1];
+ if (response.status === module.HTTP_NOT_FOUND ||
+ response.status === module.HTTP_SEE_ALSO) {
+ var file = new module.HostedFile(client, original_url);
+ return on_success(file);
+ } else if (old_on_failure !== undefined) {
+ return old_on_failure(ignore, response, args);
+ }
+ };
+ this.lp_client.get(uri, {on: on});
+ },
+
+ 'named_get': function(operation_name, config) {
+ /* Get the result of a named GET operation on this resource. */
+ return this.lp_client.named_get(this.uri, operation_name,
+ config);
+ },
+
+ 'named_post': function(operation_name, config) {
+ /* Trigger a named POST operation on this resource. */
+ return this.lp_client.named_post(this.uri, operation_name,
+ config);
+ }
+ };
+
+ // The service root resource.
+ module.Root = function(client, representation, uri) {
+ /* The root of the Launchpad web service. */
+ this.init(client, representation, uri);
+ };
+ module.Root.prototype = new module.Resource();
+
+ module.Collection = function(client, representation, uri) {
+ /* A grouped collection of objets from the Launchpad web service. */
+ var index, entry;
+ this.init(client, representation, uri);
+ for (index = 0 ; index < this.entries.length ; index++) {
+ entry = this.entries[index];
+ this.entries[index] = new module.Entry(client, entry, entry.self_link);
+ }
+ };
+
+ module.Collection.prototype = new module.Resource();
+
+ module.Collection.prototype.lp_slice = function(on, start, size) {
+ /* Retrieve a subset of the collection.
+
+ :param start: Where in the collection to start serving entries.
+ :param size: How many entries to serve.
+ */
+ return this.lp_client.get(this.uri,
+ {on: on, start: start, size: size});
+ };
+
+ module.Entry = function(client, representation, uri) {
+ /* A single object from the Launchpad web service. */
+ this.lp_client = client;
+ this.uri = uri;
+ this.dirty_attributes = [];
+ var entry = this;
+
+ // Copy the representation keys into our own set of attributes, and add
+ // an attribute-change event listener for caching purposes.
var key;
for (key in representation) {
if (representation.hasOwnProperty(key)) {
- this[key] = representation[key];
- }
- }
- },
-
- 'lookup_value': function(key) {
- /* A common getter interface for Entrys and non-Entrys. */
- return this[key];
- },
-
- 'follow_link': function(link_name, config) {
- /* Return the object at the other end of the named link. */
- var on = config.on;
- var uri = this.lookup_value(link_name + '_link');
- if (uri === undefined) {
- uri = this.lookup_value(link_name + '_collection_link');
- }
- if (uri === undefined) {
- throw new Error("No such link: " + link_name);
- }
-
- // If the response is 404, it means we have a hosted file that
- // doesn't exist yet. If the response is 303 and goes off to another
- // site, that means we have a hosted file that does exist. Either way
- // we should turn the failure into a success.
- var on_success = on.success;
- var old_on_failure = on.failure;
- on.failure = function(ignore, response, args) {
- var client = args[0];
- var original_url = args[1];
- if (response.status === module.HTTP_NOT_FOUND ||
- response.status === module.HTTP_SEE_ALSO) {
- var file = new HostedFile(client, original_url);
- return on_success(file);
- } else if (old_on_failure !== undefined) {
- return old_on_failure(ignore, response, args);
- }
- };
- this.lp_client.get(uri, {on: on});
- },
-
- 'named_get': function(operation_name, config) {
- /* Get the result of a named GET operation on this resource. */
- return this.lp_client.named_get(this.lp_original_uri, operation_name,
- config);
- },
-
- 'named_post': function(operation_name, config) {
- /* Trigger a named POST operation on this resource. */
- return this.lp_client.named_post(this.lp_original_uri, operation_name,
- config);
- }
-};
-
-module.Resource = Resource;
-
-
-// The service root resource.
-Root = function(client, representation, uri) {
- /* The root of the Launchpad web service. */
- this.init(client, representation, uri);
-};
-Root.prototype = new Resource();
-
-module.Root = Root;
-
-
-var Collection = function(client, representation, uri) {
- /* A grouped collection of objets from the Launchpad web service. */
- var index, entry;
- this.init(client, representation, uri);
- for (index = 0 ; index < this.entries.length ; index++) {
- entry = this.entries[index];
- this.entries[index] = new Entry(client, entry, entry.self_link);
- }
-};
-
-Collection.prototype = new Resource();
-
-Collection.prototype.lp_slice = function(on, start, size) {
- /* Retrieve a subset of the collection.
-
- :param start: Where in the collection to start serving entries.
- :param size: How many entries to serve.
- */
- return this.lp_client.get(this.lp_original_uri,
- {on: on, start: start, size: size});
-};
-
-module.Collection = Collection;
-
-var Entry = function(client, representation, uri) {
- /* A single object from the Launchpad web service. */
- this.lp_client = client;
- this.lp_original_uri = uri;
- this.dirty_attributes = [];
- var entry = this;
-
- // Copy the representation keys into our own set of attributes, and add
- // an attribute-change event listener for caching purposes.
- var key;
- for (key in representation) {
- if (representation.hasOwnProperty(key)) {
- this.addAttr(key, {value: representation[key]});
- this.on(key + "Change", this.mark_as_dirty);
- }
- }
-};
-
-Entry.prototype = new Resource();
-
-// Augment with Attribute so that we can listen for attribute change events.
-Y.augment(Entry, Y.Attribute);
-
-Entry.prototype.mark_as_dirty = function(event) {
- /* Respond to an event triggered by modification to an Entry's field. */
- if (event.newVal !== event.prevVal) {
- this.dirty_attributes.push(event.attrName);
- }
-};
-
-Entry.prototype.lp_save = function(config) {
- /* Write modifications to this entry back to the web service. */
- var representation = {};
- var entry = this;
- Y.each(this.dirty_attributes, function(attribute, key) {
- representation[attribute] = entry.get(attribute);
- });
- var headers = {};
- if (this.get('http_etag') !== undefined) {
- headers['If-Match'] = this.get('http_etag');
- }
- var uri = module.normalize_uri(this.get('self_link'));
- this.lp_client.patch(uri, representation, config, headers);
- this.dirty_attributes = [];
-};
-
-Entry.prototype.lookup_value = function(key) {
- /* A common getter interface between Entrys and non-Entrys. */
- return this.get(key);
-};
-
-Entry.prototype.getHTML = function(key) {
- var lp_html = this.get('lp_html');
- if (lp_html) {
- // First look for the key.
- var value = lp_html[key];
- if (value === undefined) {
- // now look for key_link
- value = lp_html[key + '_link'];
- }
- if (value !== undefined) {
- var result = Y.Node.create("<span/>");
- result.setContent(value);
- return result;
- }
- }
- return null;
-};
-
-module.Entry = Entry;
-
-// The Launchpad client itself.
-
-var Launchpad = function(config) {
- /* A client that makes HTTP requests to Launchpad's web service. */
- this.io_provider = module.get_configured_io_provider(config);
- this.sync = (config ? config.sync : false);
-};
-
-Launchpad.prototype = {
- 'get': function (uri, config) {
- /* Get the current state of a resource. */
- var on = Y.merge(config.on);
- var start = config.start;
- var size = config.size;
- var data = config.data;
- var headers = module.add_accept(config);
- uri = module.normalize_uri(uri);
- if (data === undefined) {
- data = "";
- }
- if (start !== undefined || size !== undefined) {
- data = module.start_and_size(data, start, size);
- }
-
- var old_on_success = on.success;
- var update_cache = false;
- on.success = module.wrap_resource_on_success;
- var client = this;
- var y_config = {
- on: on,
- 'arguments': [client, uri, old_on_success, update_cache],
- 'headers': headers,
- data: data,
- sync: this.sync
- };
- return this.io_provider.io(uri, y_config);
- },
-
- 'named_get' : function(uri, operation_name, config) {
- /* Retrieve the value of a named GET operation on the given URI. */
- var parameters = config.parameters;
- var data = module.append_qs("", "ws.op", operation_name);
- var name;
- for (name in parameters) {
- if (parameters.hasOwnProperty(name)) {
- data = module.append_qs(data, name, parameters[name]);
- }
- }
- config.data = data;
- return this.get(uri, config);
- },
-
- 'named_post' : function (uri, operation_name, config) {
- /* Perform a named POST operation on the given URI. */
- var on = Y.merge(config.on);
- var parameters = config.parameters;
- var data;
- var name;
- uri = module.normalize_uri(uri);
- data = module.append_qs(data, "ws.op", operation_name);
- for (name in parameters) {
- if (parameters.hasOwnProperty(name)) {
- data = module.append_qs(data, name, parameters[name]);
- }
- }
-
- var old_on_success = on.success;
-
- on.success = function(unknown, response, args) {
- if (response.status === module.HTTP_CREATED) {
- // A new object was created as a result of the operation.
- // Get that object and run the callback on it instead.
- var new_location = response.getResponseHeader("Location");
- return client.get(new_location,
- { on: { success: old_on_success,
- failure: on.failure } });
- }
- return module.wrap_resource_on_success(undefined, response, args);
- };
- var client = this;
- var update_cache = false;
- var y_config = {
- method: "POST",
- on: on,
- 'arguments': [client, uri, old_on_success, update_cache],
- data: data,
- sync: this.sync
- };
- this.io_provider.io(uri, y_config);
- },
-
- 'patch': function(uri, representation, config, headers) {
- var on = Y.merge(config.on);
- var data = Y.JSON.stringify(representation);
- uri = module.normalize_uri(uri);
-
- var old_on_success = on.success;
- var update_cache = true;
- on.success = module.wrap_resource_on_success;
- args = [this, uri, old_on_success, update_cache];
-
- var extra_headers = {
+ this.addAttr(key, {value: representation[key]});
+ this.on(key + "Change", this.mark_as_dirty);
+ }
+ }
+ };
+
+ module.Entry.prototype = new module.Resource();
+
+ // Augment with Attribute so that we can listen for attribute change events.
+ Y.augment(module.Entry, Y.Attribute);
+
+ module.Entry.prototype.mark_as_dirty = function(event) {
+ /* Respond to an event triggered by modification to an Entry's field. */
+ if (event.newVal !== event.prevVal) {
+ this.dirty_attributes.push(event.attrName);
+ }
+ };
+
+ module.Entry.prototype.lp_save = function(config) {
+ /* Write modifications to this entry back to the web service. */
+ var representation = {};
+ var entry = this;
+ Y.each(this.dirty_attributes, function(attribute, key) {
+ representation[attribute] = entry.get(attribute);
+ });
+ var headers = {};
+ if (this.get('http_etag') !== undefined) {
+ headers['If-Match'] = this.get('http_etag');
+ }
+ var uri = module.normalize_uri(this.get('self_link'));
+ this.lp_client.patch(uri, representation, config, headers);
+ this.dirty_attributes = [];
+ };
+
+ module.Entry.prototype.lookup_value = function(key) {
+ /* A common getter interface between Entrys and non-Entrys. */
+ return this.get(key);
+ };
+
+ module.Entry.prototype.getHTML = function(key) {
+ var lp_html = this.get('lp_html');
+ if (lp_html) {
+ // First look for the key.
+ var value = lp_html[key];
+ if (value === undefined) {
+ // now look for key_link
+ value = lp_html[key + '_link'];
+ }
+ if (value !== undefined) {
+ var result = Y.Node.create("<span/>");
+ result.setContent(value);
+ return result;
+ }
+ }
+ return null;
+ };
+
+ // The Launchpad client itself.
+ module.Launchpad = function(config) {
+ /* A client that makes HTTP requests to Launchpad's web service. */
+ this.io_provider = module.get_configured_io_provider(config);
+ this.sync = (config ? config.sync : false);
+ };
+
+ module.Launchpad.prototype = {
+ 'get': function (uri, config) {
+ /* Get the current state of a resource. */
+ var on = Y.merge(config.on);
+ var start = config.start;
+ var size = config.size;
+ var data = config.data;
+ var headers = module.add_accept(config);
+ uri = module.normalize_uri(uri);
+ if (data === undefined) {
+ data = "";
+ }
+ if (start !== undefined || size !== undefined) {
+ data = module.start_and_size(data, start, size);
+ }
+
+ var old_on_success = on.success;
+ var update_cache = false;
+ on.success = module.wrap_resource_on_success;
+ var client = this;
+ var y_config = {
+ on: on,
+ 'arguments': [
+ client, uri, old_on_success, update_cache, module.GET],
+ 'headers': headers,
+ data: data,
+ sync: this.sync
+ };
+ return this.io_provider.io(uri, y_config);
+ },
+
+ 'named_get' : function(uri, operation_name, config) {
+ /* Retrieve the value of a named GET operation on the given URI. */
+ var parameters = config.parameters;
+ var data = module.append_qs("", "ws.op", operation_name);
+ var name;
+ for (name in parameters) {
+ if (parameters.hasOwnProperty(name)) {
+ data = module.append_qs(data, name, parameters[name]);
+ }
+ }
+ config.data = data;
+ return this.get(uri, config);
+ },
+
+ 'named_post' : function (uri, operation_name, config) {
+ /* Perform a named POST operation on the given URI. */
+ var on = Y.merge(config.on);
+ var parameters = config.parameters;
+ var data;
+ var name;
+ uri = module.normalize_uri(uri);
+ data = module.append_qs(data, "ws.op", operation_name);
+ for (name in parameters) {
+ if (parameters.hasOwnProperty(name)) {
+ data = module.append_qs(data, name, parameters[name]);
+ }
+ }
+
+ var old_on_success = on.success;
+
+ on.success = function(unknown, response, args) {
+ if (response.status === module.HTTP_CREATED) {
+ // A new object was created as a result of the operation.
+ // Get that object and run the callback on it instead.
+ var new_location = response.getResponseHeader("Location");
+ return client.get(new_location,
+ { on: { success: old_on_success,
+ failure: on.failure } });
+ }
+ return module.wrap_resource_on_success(
+ undefined, response, args, module.POST);
+ };
+ var client = this;
+ var update_cache = false;
+ var y_config = {
+ method: "POST",
+ on: on,
+ 'arguments': [client, uri, old_on_success, update_cache],
+ data: data,
+ sync: this.sync
+ };
+ this.io_provider.io(uri, y_config);
+ },
+
+ 'patch': function(uri, representation, config, headers) {
+ var on = Y.merge(config.on);
+ var data = Y.JSON.stringify(representation);
+ uri = module.normalize_uri(uri);
+
+ var old_on_success = on.success;
+ var update_cache = true;
+ on.success = module.wrap_resource_on_success;
+ var args = [this, uri, old_on_success, update_cache, module.PATCH];
+
+ var extra_headers = {
"X-HTTP-Method-Override": "PATCH",
"Content-Type": "application/json",
"X-Content-Type-Override": "application/json"
- };
- var name;
- if (headers !== undefined) {
- for (name in headers) {
- if (headers.hasOwnProperty(name)) {
- extra_headers[name] = headers[name];
+ };
+ var name;
+ if (headers !== undefined) {
+ for (name in headers) {
+ if (headers.hasOwnProperty(name)) {
+ extra_headers[name] = headers[name];
+ }
}
}
- }
- extra_headers = module.add_accept(config, extra_headers);
-
- var y_config = {
- 'method': "POST",
- 'on': on,
- 'headers': extra_headers,
- 'arguments': args,
- 'data': data,
- 'sync': this.sync
- };
- this.io_provider.io(uri, y_config);
- },
-
- 'wrap_resource': function(uri, representation) {
- var key;
- var new_representation;
- /* Given a representation, turn it into a subclass of Resource. */
- if (representation === null || representation === undefined) {
- return representation;
- }
- if (representation.resource_type_link === undefined) {
- // This is a non-entry object returned by a named operation.
- // It's either a list or a random JSON object.
- if (representation.total_size !== undefined
- || representation.total_size_link !== undefined) {
- // It's a list. Treat it as a collection;
- // it should be slicable.
- return new Collection(this, representation, uri);
- } else if (Y.Lang.isObject(representation)) {
- // It's an Array or mapping. Recurse into it.
- if (Y.Lang.isArray(representation)) {
- new_representation = [];
- }
- else {
- new_representation = {};
- }
- for (key in representation) {
- if (representation.hasOwnProperty(key)) {
- var value = representation[key];
- if (Y.Lang.isValue(value)) {
- value = this.wrap_resource(
- value.self_link, value);
- }
- new_representation[key] = value;
- }
- }
- return new_representation;
- } else {
- // It's a random JSON object. Leave it alone.
+ extra_headers = module.add_accept(config, extra_headers);
+
+ var y_config = {
+ 'method': "POST",
+ 'on': on,
+ 'headers': extra_headers,
+ 'arguments': args,
+ 'data': data,
+ 'sync': this.sync
+ };
+ this.io_provider.io(uri, y_config);
+ },
+
+ 'wrap_resource': function(uri, representation) {
+ var key;
+ var new_representation;
+ /* Given a representation, turn it into a subclass of Resource. */
+ if (representation === null || representation === undefined) {
return representation;
}
- } else if (representation.resource_type_link.search(
- /\/#service-root$/) !== -1) {
- return new Root(this, representation, uri);
- } else if (representation.total_size === undefined) {
- return new Entry(this, representation, uri);
- } else {
- return new Collection(this, representation, uri);
- }
- }
-};
-
-module.Launchpad = Launchpad;
-
-
-/**
- * Helper object for handling XHR failures.
- * clearProgressUI() and showError() need to be defined by the callsite
- * using this object.
- *
- * @class ErrorHandler
- */
-var ErrorHandler;
-ErrorHandler = function(config) {
- ErrorHandler.superclass.constructor.apply(this, arguments);
-};
-
-Y.extend(ErrorHandler, Y.Base, {
- /**
- * Clear the progress indicator.
- *
- * The default implementation does nothing. Override this to provide
- * an implementation to remove the UI elements used to indicate
- * progress. After this method is called, the UI should be ready for
- * repeating the interaction, allowing the user to retry submitting
- * the data.
- *
- * @method clearProgressUI
- */
- clearProgressUI: function () {},
-
- /**
- * Show the error message to the user.
- *
- * The default implementation does nothing. Override this to provide
- * an implementation to display the UI elements containing the error
- * message.
- *
- * @method showError
- * @param error_msg The error text to display.
- */
- showError: function (error_msg) {},
-
- /**
- * Handle an error from an XHR request.
- *
- * This method is invoked before any generic error handling is done.
- *
- * @method handleError
- * @param ioId The request id.
- * @param response The XHR call response object.
- * @return {Boolean} Return true if the error has been fully processed and
- * any further generic error handling is not required.
- */
- handleError: function(ioId, response) {
- return false;
- },
-
- /**
- * Return a failure handler function for XHR requests.
- *
- * Assign the result of this function as the failure handler when
- * doing an XHR request using the API client.
- *
- * @method getFailureHandler
- */
- getFailureHandler: function () {
- var self = this;
- return function(ioId, o) {
- self.clearProgressUI();
- // Perform any user specified error handling. If true is returned,
- // we do not do any further processing.
- if( self.handleError(ioId, o) ) {
+ if (representation.resource_type_link === undefined) {
+ // This is a non-entry object returned by a named operation.
+ // It's either a list or a random JSON object.
+ if (representation.total_size !== undefined
+ || representation.total_size_link !== undefined) {
+ // It's a list. Treat it as a collection;
+ // it should be slicable.
+ return new module.Collection(this, representation, uri);
+ } else if (Y.Lang.isObject(representation)) {
+ // It's an Array or mapping. Recurse into it.
+ if (Y.Lang.isArray(representation)) {
+ new_representation = [];
+ }
+ else {
+ new_representation = {};
+ }
+ for (key in representation) {
+ if (representation.hasOwnProperty(key)) {
+ var value = representation[key];
+ if (Y.Lang.isValue(value)) {
+ value = this.wrap_resource(
+ value.self_link, value);
+ }
+ new_representation[key] = value;
+ }
+ }
+ return new_representation;
+ } else {
+ // It's a random JSON object. Leave it alone.
+ return representation;
+ }
+ } else if (representation.resource_type_link.search(
+ /\/#service-root$/) !== -1) {
+ return new module.Root(this, representation, uri);
+ } else if (representation.total_size === undefined) {
+ return new module.Entry(this, representation, uri);
+ } else {
+ return new module.Collection(this, representation, uri);
+ }
+ }
+ };
+
+ /**
+ * Helper object for handling XHR failures.
+ * clearProgressUI() and showError() need to be defined by the callsite
+ * using this object.
+ *
+ * @class ErrorHandler
+ */
+ module.ErrorHandler = Y.Base.create('lp-client-error-handler', Y.Base, [], {
+ /**
+ * Clear the progress indicator.
+ *
+ * The default implementation does nothing. Override this to provide
+ * an implementation to remove the UI elements used to indicate
+ * progress. After this method is called, the UI should be ready for
+ * repeating the interaction, allowing the user to retry submitting
+ * the data.
+ *
+ * @method clearProgressUI
+ */
+ clearProgressUI: function () {},
+
+ /**
+ * Show the error message to the user.
+ *
+ * The default implementation does nothing. Override this to provide
+ * an implementation to display the UI elements containing the error
+ * message.
+ *
+ * @method showError
+ * @param error_msg The error text to display.
+ */
+ showError: function (error_msg) {},
+
+ /**
+ * Handle an error from an XHR request.
+ *
+ * This method is invoked before any generic error handling is done.
+ *
+ * @method handleError
+ * @param ioId The request id.
+ * @param response The XHR call response object.
+ * @return {Boolean} Return true if the error has been fully processed and
+ * any further generic error handling is not required.
+ */
+ handleError: function(ioId, response) {
+ return false;
+ },
+
+ /**
+ * Return a failure handler function for XHR requests.
+ *
+ * Assign the result of this function as the failure handler when
+ * doing an XHR request using the API client.
+ *
+ * @method getFailureHandler
+ */
+ getFailureHandler: function () {
+ var self = this;
+ return function(ioId, o) {
+ self.clearProgressUI();
+ // Perform any user specified error handling. If true is returned,
+ // we do not do any further processing.
+ if( self.handleError(ioId, o) ) {
+ return;
+ }
+ // If it was a timeout...
+ if (o.status === 503) {
+ self.showError(
+ 'Timeout error, please try again in a few minutes.');
+ // If it was a server error...
+ } else if (o.status >= 500) {
+ var server_error =
+ 'Server error, please contact an administrator.';
+ var oops_id = self.get_oops_id(o);
+ if (oops_id) {
+ server_error = server_error + ' OOPS ID:' + oops_id;
+ }
+ self.showError(server_error);
+ // Otherwise we send some sane text as an error
+ } else if (o.status === 412){
+ self.showError(o.status + ' ' + o.statusText);
+ } else {
+ self.showError(self.get_generic_error(o));
+ }
+ };
+ },
+ get_oops_id: function(response) {
+ return response.getResponseHeader('X-Lazr-OopsId');
+ },
+ get_generic_error: function(response) {
+ return response.responseText;
+ }
+ });
+
+ module.FormErrorHandler = Y.Base.create('lp-client-error-form',
+ module.ErrorHandler, [], {
+ // Clear any errors on the form.
+ clearFormErrors: function() {
+ Y.all('.error.message').remove(true);
+ Y.all('.error .message').remove(true);
+ Y.all('div.error').removeClass('error');
+ },
+
+ // If the XHR call returns a form validation error, we display the errors
+ // on the form.
+ handleError: function(ioId, response) {
+ if (response.status === 400
+ && response.statusText === 'Validation') {
+ var response_info = Y.JSON.parse(response.responseText);
+ var error_summary = response_info.error_summary;
+ var form_wide_errors = response_info.form_wide_errors;
+ var errors = response_info.errors;
+ this.handleFormValidationError(
+ error_summary, form_wide_errors, errors);
+ return true;
+ }
+ return false;
+ },
+
+ // Display the specified errors on the form. The errors are displayed in
+ // the same way as is done by the Launchpad HTML form rendering
+ // infrastructure using TAL templates.
+ handleFormValidationError: function(error_summary,
+ form_wide_errors, errors) {
+ var form = this.get('form');
+ if (!Y.Lang.isValue(form)) {
+ form = Y.one("[name='launchpadform']");
+ }
+ if (!Y.Lang.isValue(form)) {
return;
}
- // If it was a timeout...
- if (o.status === 503) {
- self.showError(
- 'Timeout error, please try again in a few minutes.');
- // If it was a server error...
- } else if (o.status >= 500) {
- var server_error =
- 'Server error, please contact an administrator.';
- var oops_id = self.get_oops_id(o);
- if (oops_id) {
- server_error = server_error + ' OOPS ID:' + oops_id;
+ var form_content = form.one('table.form');
+ if (!Y.Lang.isValue(form_content)) {
+ form_content = form;
+ }
+ // Display the error summary information.
+ var error_summary_node =
+ Y.Node.create('<p class="error message"></p>')
+ .set('text', error_summary);
+ form_content.insertBefore(error_summary_node, form_content);
+ // Display the form wide errors.
+ if (form_wide_errors.length > 0) {
+ var form_error_node =
+ Y.Node.create('<div class="error message"></div>');
+ Y.Array.each(form_wide_errors, function(message) {
+ form_error_node.appendChild(Y.Node.create('<p></p>')
+ .set('text', message));
+ });
+ form_content.insertBefore(form_error_node, form_content);
+ }
+ // Display the field specific errors.
+ Y.each(errors, function(message, field_name) {
+ var label = Y.one('label[for="' + field_name + '"]');
+ if (Y.Lang.isValue(label)) {
+ label.ancestor('div').addClass('error');
+ var field = label.next('div');
+ var error_node =
+ Y.Node.create('<div class="message"></div>')
+ .set('text', message);
+ field.insert(error_node, 'after');
}
- self.showError(server_error);
- // Otherwise we send some sane text as an error
- } else if (o.status === 412){
- self.showError(o.status + ' ' + o.statusText);
- } else {
- self.showError(self.get_generic_error(o));
- }
- };
- },
- get_oops_id: function(response) {
- return response.getResponseHeader('X-Lazr-OopsId');
- },
- get_generic_error: function(response) {
- return response.responseText;
- }
-});
-
-module.ErrorHandler = ErrorHandler;
-
-var FormErrorHandler;
-FormErrorHandler = function(config) {
- FormErrorHandler.superclass.constructor.apply(this, arguments);
-};
-
-FormErrorHandler.ATTRS = {
- form: {
- value: null
- }
-};
-
-Y.extend(FormErrorHandler, ErrorHandler, {
-
- // Clear any errors on the form.
- clearFormErrors: function() {
- Y.all('.error.message').remove(true);
- Y.all('.error .message').remove(true);
- Y.all('div.error').removeClass('error');
- },
-
- // If the XHR call returns a form validation error, we display the errors
- // on the form.
- handleError: function(ioId, response) {
- if (response.status === 400
- && response.statusText === 'Validation') {
- var response_info = Y.JSON.parse(response.responseText);
- var error_summary = response_info.error_summary;
- var form_wide_errors = response_info.form_wide_errors;
- var errors = response_info.errors;
- this.handleFormValidationError(
- error_summary, form_wide_errors, errors);
- return true;
- }
- return false;
- },
-
- // Display the specified errors on the form. The errors are displayed in
- // the same way as is done by the Launchpad HTML form rendering
- // infrastructure using TAL templates.
- handleFormValidationError: function(error_summary,
- form_wide_errors, errors) {
- var form = this.get('form');
- if (!Y.Lang.isValue(form)) {
- form = Y.one("[name='launchpadform']");
- }
- if (!Y.Lang.isValue(form)) {
- return;
- }
- var form_content = form.one('table.form');
- if (!Y.Lang.isValue(form_content)) {
- form_content = form;
- }
- // Display the error summary information.
- var error_summary_node =
- Y.Node.create('<p class="error message"></p>')
- .set('text', error_summary);
- form_content.insertBefore(error_summary_node, form_content);
- // Display the form wide errors.
- if (form_wide_errors.length > 0) {
- var form_error_node =
- Y.Node.create('<div class="error message"></div>');
- Y.Array.each(form_wide_errors, function(message) {
- form_error_node.appendChild(Y.Node.create('<p></p>')
- .set('text', message));
});
- form_content.insertBefore(form_error_node, form_content);
- }
- // Display the field specific errors.
- Y.each(errors, function(message, field_name) {
- var label = Y.one('label[for="' + field_name + '"]');
- if (Y.Lang.isValue(label)) {
- label.ancestor('div').addClass('error');
- var field = label.next('div');
- var error_node =
- Y.Node.create('<div class="message"></div>')
- .set('text', message);
- field.insert(error_node, 'after');
- }
- });
- },
-
- get_oops_id: function(response) {
- var oops_re = /code class\="oopsid">(OOPS-[^<]*)/;
- var result = response.responseText.match(oops_re);
- if (result === null) {
- return null;
- }
- return result[1];
- },
-
- get_generic_error: function(response) {
- if (response.status !== 403){
- return "Sorry, you don't have permission to make this change.";
- }
- else {
- return response.status + ' ' + response.statusText;
- }
- }
+ },
+
+ get_oops_id: function(response) {
+ var oops_re = /code class\="oopsid">(OOPS-[^<]*)/;
+ var result = response.responseText.match(oops_re);
+ if (result === null) {
+ return null;
+ }
+ return result[1];
+ },
+
+ get_generic_error: function(response) {
+ if (response.status !== 403){
+ return "Sorry, you don't have permission to make this change.";
+ }
+ else {
+ return response.status + ' ' + response.statusText;
+ }
+ }
+
+ }, {
+ ATTRS: {
+ form: {
+ value: null
+ }
+ }
+ });
+
+}, "0.1", {
+ requires: ["base", "attribute", "io", "querystring", "json-parse",
+ "json-stringify", "lp"]
});
-module.FormErrorHandler = FormErrorHandler;
-
-
-}, "0.1",
- {"requires":["attribute", "io", "querystring", "json-parse",
- "json-stringify", "lp"]});
-
YUI.add('lp.client.plugins', function (Y) {
-
-/**
- * A collection of plugins to hook lp.client into widgets.
- *
- * @module lp.client.plugins
- */
-
-/**
- * This plugin overrides the widget _saveData method to update the
- * underlying model object using a PATCH call.
- *
- * @namespace lp.client.plugins
- * @class PATCHPlugin
- * @extends Widget
- */
-var PATCHPlugin = function PATCHPlugin () {
- PATCHPlugin.superclass.constructor.apply(this, arguments);
-};
-
-Y.mix(PATCHPlugin, {
- /**
- * The identity of the plugin.
- *
- * @property PATCHPlugin.NAME
- * @type String
- * @static
- */
- NAME: 'PATCHPlugin',
-
- /**
- * The namespace of the plugin.
- *
- * @property PATCHPlugin.NS
- * @type String
- * @static
- */
- NS: 'patcher',
-
- /**
- * Static property used to define the default attribute configuration of
- * this plugin.
- *
- * @property PATCHPlugin.ATTRS
- * @type Object
- * @static
- */
- ATTRS : {
- /**
- * Name of the attribute to patch.
- *
- * @attribute patch
- * @type String
- */
- patch: {},
-
- /**
- * URL of the resource to PATCH.
- *
- * @attribute resource
- * @type String
- */
- resource: {},
-
- /**
- * Should the resulting field get the value from the lp_html
- * attribute?
- *
- * @attribute use_html
- * @type Boolean
- */
- use_html: false,
-
- /**
- * The function to use to format the returned result into a form that
- * can be inserted into the page DOM.
- *
- * The default value is a function that simply returns the result
- * unmodified.
- *
- * @attribute formatter
- * @type Function
- * @default null
- */
- formatter: {
- valueFn: function() { return this._defaultFormatter; }
- }
-}});
-
-Y.extend(PATCHPlugin, Y.Plugin.Base, {
-
- /**
- * Configuration parameters that will be passed through to the lp.client
- * call.
- *
- * @property extra_config
- * @type Hash
- */
- extra_config: null,
-
- /**
- * Constructor code. Check that the required config parameters are
- * present and wrap the host _saveData method.
- *
- * @method initializer
- * @protected
- */
- initializer: function(config) {
- if (!Y.Lang.isString(config.patch)) {
- Y.error("missing config: 'patch' containing the attribute name");
- }
-
- if (!Y.Lang.isString(config.resource)) {
- Y.error("missing config: 'resource' containing the URL to patch");
- }
-
- // Save the config object that the user passed in so that we can pass
- // any extra parameters through to the lp.client constructor.
- this.extra_config = config || {};
- this.extra_config.accept = 'application/json;include=lp_html';
-
- // Save a reference to the original _saveData()
- //method before wrapping it.
- this.original_save = config.host._saveData;
-
- // We want to run our PATCH code instead of the original
- // 'save' method. Using doBefore() means that
- // unplugging our code will leave the original
- // widget in a clean state.
- this.doBefore("_saveData", this.doPATCH);
-
- var self = this;
- this.error_handler = new Y.lp.client.ErrorHandler();
- this.error_handler.clearProgressUI = function () {
- config.host._uiClearWaiting();
- };
- this.error_handler.showError = function (error_msg) {
- config.host.showError(error_msg);
- };
- },
-
- /**
- * Send a PATCH request with the widget's input value for the
- * configured attribute.
- *
- * It will set the widget in waiting status, do the PATCH.
- * Success will call the original widget save method.
- *
- * Errors are reported through the widget's showError() method.
- *
- * @method doPATCH
- */
- doPATCH: function() {
- var owner = this.get("host"),
- original_save = this.original_save;
-
- // Set the widget in 'waiting' state.
- owner._uiSetWaiting();
-
- var client = new Y.lp.client.Launchpad();
- var formatter = Y.bind(this.get('formatter'), this);
- var attribute = this.get('patch');
-
- var patch_payload;
- var val = owner.getInput();
- patch_payload = {};
- patch_payload[attribute] = val;
-
- var callbacks = {
- on: {
- success: function (entry) {
- owner._uiClearWaiting();
- var new_value = formatter(entry, attribute);
- original_save.apply(owner, [new_value]);
- },
- failure: this.error_handler.getFailureHandler()
- }
- };
-
- var cfg = Y.merge(callbacks, this.extra_config);
-
- client.patch(this.get('resource'), patch_payload, cfg);
-
- // Prevent the method we are hooking before from running.
- return new Y.Do.Halt();
- },
-
- /**
- * Return the webservice Entry object attribute that is to be shown in the
- * page DOM.
- *
- * This function may be overridden in various ways.
- *
- * @method _defaultFormatter
- * @protected
- * @param result {Entry|String} A Launchpad webservice Entry object, or
- * the unmodified result string if the default Content-Type wasn't used.
- * @param attribute {String} The resource attribute that the PATCH request
- * was sent to.
- * @return {String|Node} A string or Node instance to be inserted into
- * the DOM.
- */
- _defaultFormatter: function(result, attribute) {
- if (Y.Lang.isString(result)) {
- return result;
- } else {
- if (this.get('use_html')) {
- return result.getHTML(attribute).get('innerHTML');
- } else {
- return result.get(attribute);
- }
- }
- }
+ /**
+ * A collection of plugins to hook lp.client into widgets.
+ *
+ * @module lp.client.plugins
+ */
+ var module = Y.namespace('lp.client.plugins');
+
+ /**
+ * This plugin overrides the widget _saveData method to update the
+ * underlying model object using a PATCH call.
+ *
+ * @namespace lp.client.plugins
+ * @class PATCHPlugin
+ * @extends Widget
+ */
+ module.PATCHPlugin = Y.Base.create('client-patch-plugin', Y.Base, [], {
+ /**
+ * Configuration parameters that will be passed through to the lp.client
+ * call.
+ *
+ * @property extra_config
+ * @type Hash
+ */
+ extra_config: null,
+
+ /**
+ * Constructor code. Check that the required config parameters are
+ * present and wrap the host _saveData method.
+ *
+ * @method initializer
+ * @protected
+ */
+ initializer: function(config) {
+ if (!Y.Lang.isString(config.patch)) {
+ Y.error("missing config: 'patch' containing the attribute name");
+ }
+
+ if (!Y.Lang.isString(config.resource)) {
+ Y.error("missing config: 'resource' containing the URL to patch");
+ }
+
+ // Save the config object that the user passed in so that we can pass
+ // any extra parameters through to the lp.client constructor.
+ this.extra_config = config || {};
+ this.extra_config.accept = 'application/json;include=lp_html';
+
+ // Save a reference to the original _saveData()
+ //method before wrapping it.
+ this.original_save = config.host._saveData;
+
+ // We want to run our PATCH code instead of the original
+ // 'save' method. Using doBefore() means that
+ // unplugging our code will leave the original
+ // widget in a clean state.
+ this.doBefore("_saveData", this.doPATCH);
+
+ var self = this;
+ this.error_handler = new Y.lp.client.ErrorHandler();
+ this.error_handler.clearProgressUI = function () {
+ config.host._uiClearWaiting();
+ };
+ this.error_handler.showError = function (error_msg) {
+ config.host.showError(error_msg);
+ };
+ },
+
+ /**
+ * Send a PATCH request with the widget's input value for the
+ * configured attribute.
+ *
+ * It will set the widget in waiting status, do the PATCH.
+ * Success will call the original widget save method.
+ *
+ * Errors are reported through the widget's showError() method.
+ *
+ * @method doPATCH
+ */
+ doPATCH: function() {
+ var owner = this.get("host"),
+ original_save = this.original_save;
+
+ // Set the widget in 'waiting' state.
+ owner._uiSetWaiting();
+
+ var client = new Y.lp.client.Launchpad();
+ var formatter = Y.bind(this.get('formatter'), this);
+ var attribute = this.get('patch');
+
+ var patch_payload;
+ var val = owner.getInput();
+ patch_payload = {};
+ patch_payload[attribute] = val;
+
+ var callbacks = {
+ on: {
+ success: function (entry) {
+ owner._uiClearWaiting();
+ var new_value = formatter(entry, attribute);
+ original_save.apply(owner, [new_value]);
+ },
+ failure: this.error_handler.getFailureHandler()
+ }
+ };
+
+ var cfg = Y.merge(callbacks, this.extra_config);
+
+ client.patch(this.get('resource'), patch_payload, cfg);
+
+ // Prevent the method we are hooking before from running.
+ return new Y.Do.Halt();
+ },
+
+ /**
+ * Return the webservice Entry object attribute that is to be shown in the
+ * page DOM.
+ *
+ * This function may be overridden in various ways.
+ *
+ * @method _defaultFormatter
+ * @protected
+ * @param result {Entry|String} A Launchpad webservice Entry object, or
+ * the unmodified result string if the default Content-Type wasn't used.
+ * @param attribute {String} The resource attribute that the PATCH request
+ * was sent to.
+ * @return {String|Node} A string or Node instance to be inserted into
+ * the DOM.
+ */
+ _defaultFormatter: function(result, attribute) {
+ if (Y.Lang.isString(result)) {
+ return result;
+ } else {
+ if (this.get('use_html')) {
+ return result.getHTML(attribute).get('innerHTML');
+ } else {
+ return result.get(attribute);
+ }
+ }
+ }
+
+ }, {
+ /**
+ * The identity of the plugin.
+ *
+ * @property PATCHPlugin.NAME
+ * @type String
+ * @static
+ */
+ NAME: 'PATCHPlugin',
+
+ /**
+ * The namespace of the plugin.
+ *
+ * @property PATCHPlugin.NS
+ * @type String
+ * @static
+ */
+ NS: 'patcher',
+
+ /**
+ * Static property used to define the default attribute configuration of
+ * this plugin.
+ *
+ * @property PATCHPlugin.ATTRS
+ * @type Object
+ * @static
+ */
+ ATTRS : {
+ /**
+ * Name of the attribute to patch.
+ *
+ * @attribute patch
+ * @type String
+ */
+ patch: {},
+
+ /**
+ * URL of the resource to PATCH.
+ *
+ * @attribute resource
+ * @type String
+ */
+ resource: {},
+
+ /**
+ * Should the resulting field get the value from the lp_html
+ * attribute?
+ *
+ * @attribute use_html
+ * @type Boolean
+ */
+ use_html: false,
+
+ /**
+ * The function to use to format the returned result into a form that
+ * can be inserted into the page DOM.
+ *
+ * The default value is a function that simply returns the result
+ * unmodified.
+ *
+ * @attribute formatter
+ * @type Function
+ * @default null
+ */
+ formatter: {
+ valueFn: function() { return this._defaultFormatter; }
+ }
+ }
+ });
+}, "0.1", {
+ requires: ["plugin", "dump", "lazr.editor", "lp.client"]
});
-
-Y.namespace('lp.client.plugins');
-Y.lp.client.plugins.PATCHPlugin = PATCHPlugin;
-
- }, "0.1", {"requires": [
- "plugin", "dump", "lazr.editor", "lp.client"]});
=== modified file 'lib/lp/app/javascript/tests/test_lp_client_integration.js'
--- lib/lp/app/javascript/tests/test_lp_client_integration.js 2012-01-10 14:24:19 +0000
+++ lib/lp/app/javascript/tests/test_lp_client_integration.js 2012-07-23 17:08:21 +0000
@@ -138,7 +138,7 @@
Y.Assert.isTrue(config.successful, 'Getting milestone failed');
var milestone = config.result;
Y.Assert.isInstanceOf(Y.lp.client.Entry, milestone);
- Y.Assert.areSame(data.milestone_self_link, milestone.lp_original_uri);
+ Y.Assert.areSame(data.milestone_self_link, milestone.uri);
},
test_named_post_integration: function() {
@@ -321,7 +321,7 @@
var team = config.result;
Y.Assert.isInstanceOf(Y.lp.client.Entry, team);
Y.Assert.areEqual('My lpclient team', team.get('display_name'));
- Y.Assert.isTrue(/\~newlpclientteam$/.test(team.lp_original_uri));
+ Y.Assert.isTrue(/\~newlpclientteam$/.test(team.uri));
},
test_collection_paged_named_get: function() {
=== modified file 'lib/lp/registry/javascript/tests/test_structural_subscription.js'
--- lib/lp/registry/javascript/tests/test_structural_subscription.js 2012-06-28 15:30:15 +0000
+++ lib/lp/registry/javascript/tests/test_structural_subscription.js 2012-07-23 17:08:21 +0000
@@ -234,7 +234,7 @@
Y.one('body').appendChild(this.content_node);
this.bug_filter = {
- lp_original_uri:
+ uri:
'/api/devel/firefox/+subscription/mark/+filter/28'
};
this.form_data = {
=== modified file 'lib/lp/soyuz/javascript/lp_dynamic_dom_updater.js'
--- lib/lp/soyuz/javascript/lp_dynamic_dom_updater.js 2012-07-05 14:28:57 +0000
+++ lib/lp/soyuz/javascript/lp_dynamic_dom_updater.js 2012-07-23 17:08:21 +0000
@@ -266,8 +266,8 @@
}
// Finally, call the LP api method as required...
- if (uri){
- if (api_method_name){
+ if (uri) {
+ if (api_method_name) {
this.get("lp_client").named_get(uri,
api_method_name, this._lp_api_config);
}
Follow ups