← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~abentley/launchpad/reload-cache into lp:launchpad

 

Aaron Bentley has proposed merging lp:~abentley/launchpad/reload-cache into lp:launchpad with lp:~abentley/launchpad/getnewcache as a prerequisite.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~abentley/launchpad/reload-cache/+merge/69527

= Summary =
Support loading a new copy of the LP.cache.

== Proposed fix ==
See above.

== Pre-implementation notes ==

== Implementation details ==
This builds on ~bac/launchpad/getnewcache, which provides a ++model++ namespace
similar to the ++form++ namespace.  The ++model++ namespace provides the view's
IJSONRequestCache as JSON, serialized exactly how it would be serialized in the
page.

This branch merges some last-minute changes I made to json-serialization.  To
ensure the correct diff is shown, I've merged those changes into
~abentley/launchpad/getnewcache and used it, rather than bac's version, as the
prerequisite branch.

The new method is lp.client.load_model().  To support it, I extracted
get_view_url and get_form_url.  It takes an optional 'io' config parameter,
which makes testing easier.

The IJSONRequestCache serialization supports putting plain Python object types
in the cache, so I updated the deserialization, 'wrap_resource', to work the
same way.  This made convert_cache redundant, so I removed it.

To test load_model, I implemented IORecorder, which provides a mock version of
Y.io.  It serves the same purpose as MockIO, but has an imperative programming
style and supports multiple concurrent requests.  I moved MockHttpResponse into
IORecorder's file.

== Tests ==
bin/test -t test_lp_client

== Demo and Q/A ==
None

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/app/javascript/tests/test_lp_client.js
  lib/lp/soyuz/browser/distroseriessourcepackagerelease.py
  lib/lp/registry/browser/sourcepackage.py
  lib/lp/registry/browser/peoplemerge.py
  lib/canonical/launchpad/webapp/publisher.py
  lib/canonical/launchpad/webapp/templates/launchpad-model.pt
  lib/lp/code/browser/bazaar.py
  lib/lp/registry/browser/codeofconduct.py
  lib/lp/soyuz/browser/sourcepackage.py
  lib/lp/registry/browser/distributionsourcepackage.py
  lib/canonical/launchpad/webapp/tests/test_publisher.py
  lib/lp/app/javascript/client.js
  lib/lp/translations/javascript/tests/test_sourcepackage_sharing_details.js
  lib/lp/app/javascript/tests/test_lp_client.html
  lib/lp/soyuz/browser/distroseriesbinarypackage.py
  lib/lp/app/templates/base-layout-macros.pt
  lib/lp/testing/__init__.py
  lib/lp/bugs/browser/configure.zcml
  lib/canonical/launchpad/webapp/tests/test_view_model.py
  lib/lp/bugs/browser/cve.py
  lib/lp/soyuz/browser/distroarchseriesbinarypackagerelease.py
  lib/lp/soyuz/browser/distroarchseriesbinarypackage.py
  lib/lp/translations/browser/tests/test_sharing_details.py
  lib/canonical/launchpad/webapp/configure.zcml
  lib/lp/translations/browser/translationgroup.py
  lib/lp/app/webservice/tests/test_marshallers.py
  lib/lp/blueprints/browser/configure.zcml
  lib/lp/app/javascript/testing/iorecorder.js
  lib/lp/translations/javascript/sourcepackage_sharing_details.js
  lib/lp/registry/browser/teammembership.py
  lib/lp/app/webservice/marshallers.py
  lib/lp/translations/browser/translations.py
  lib/canonical/launchpad/webapp/namespace.py
  lib/lp/translations/browser/sourcepackage.py
  lib/lp/registry/browser/tests/test_subscription_links.py

./lib/lp/translations/browser/tests/test_sharing_details.py
     218: E251 no spaces around keyword / parameter equals
     226: E251 no spaces around keyword / parameter equals
     254: E251 no spaces around keyword / parameter equals
     265: E251 no spaces around keyword / parameter equals
     277: E251 no spaces around keyword / parameter equals
     311: E251 no spaces around keyword / parameter equals
-- 
https://code.launchpad.net/~abentley/launchpad/reload-cache/+merge/69527
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~abentley/launchpad/reload-cache into lp:launchpad.
=== modified file 'lib/lp/app/javascript/client.js'
--- lib/lp/app/javascript/client.js	2011-04-26 20:33:18 +0000
+++ lib/lp/app/javascript/client.js	2011-07-27 18:39:38 +0000
@@ -164,6 +164,50 @@
     }
 };
 
+
+/**
+ * Get the URL of the view for an Entry
+ * @method get_view_url
+ * @param {Entry} entry
+ * @param {String} view_name
+ * @param {String} namespace
+ * @return {String} URL
+ */
+module.get_view_url = function(entry, view_name, namespace){
+    entry_url = Y.lp.get_url_path(entry.get('web_link'));
+    return entry_url + '/' + view_name + '/++' + namespace + '++';
+};
+
+
+/**
+ * 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');
+};
+
+
+module.load_model = function(entry, view_name, config){
+    var url = module.get_view_url(entry, view_name, 'model');
+    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 = config.io;
+    if (!Y.Lang.isValue(io)) {
+        io = Y.io;
+    }
+    io(url, y_config);
+};
+
+
 module.add_accept = function(config, headers) {
     if (headers === undefined) {
         headers = {};
@@ -702,6 +746,8 @@
     },
 
     '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;
@@ -717,6 +763,25 @@
                 // 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.
                 return representation;
@@ -844,7 +909,8 @@
 module.FormErrorHandler = FormErrorHandler;
 
 
-}, "0.1" ,{"requires":["attribute", "io", "json-parse", "json-stringify"]});
+}, "0.1",
+    {"requires":["attribute", "io", "json-parse", "json-stringify", "lp"]});
 
 
 YUI.add('lp.client.plugins', function (Y) {

=== added file 'lib/lp/app/javascript/testing/iorecorder.js'
--- lib/lp/app/javascript/testing/iorecorder.js	1970-01-01 00:00:00 +0000
+++ lib/lp/app/javascript/testing/iorecorder.js	2011-07-27 18:39:38 +0000
@@ -0,0 +1,47 @@
+/* Copyright (c) 2011, Canonical Ltd. All rights reserved. */
+
+YUI.add('lp.testing.iorecorder', function(Y) {
+    namespace = Y.namespace("lp.testing.iorecorder");
+    function MockHttpResponse () {
+        this.responseText = '[]';
+        this.responseHeaders = {};
+    }
+
+    MockHttpResponse.prototype = {
+        setResponseHeader: function (header, value) {
+            this.responseHeaders[header] = value;
+        },
+
+        getResponseHeader: function(header) {
+            return this.responseHeaders[header];
+        }
+    };
+    namespace.MockHttpResponse = MockHttpResponse;
+
+
+    function IORecorderRequest(url, config){
+        this.url = url;
+        this.config = config;
+        this.response = null;
+    }
+
+
+    IORecorderRequest.prototype.success = function(value, headers){
+        this.response = new MockHttpResponse();
+        this.response.setResponseHeader(
+            'Content-Type', headers['Content-Type']);
+        this.response.responseText = value;
+        this.config.on.success(null, this.response, this.config['arguments']);
+    };
+
+
+    function IORecorder(){
+        this.requests = [];
+    }
+
+
+    IORecorder.prototype.do_io = function(url, config){
+        this.requests.push(new IORecorderRequest(url, config));
+    };
+    namespace.IORecorder = IORecorder;
+}, '0.1', {});

=== modified file 'lib/lp/app/javascript/tests/test_lp_client.html'
--- lib/lp/app/javascript/tests/test_lp_client.html	2011-07-08 06:06:15 +0000
+++ lib/lp/app/javascript/tests/test_lp_client.html	2011-07-27 18:39:38 +0000
@@ -1,7 +1,7 @@
 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd";>
 <html>
   <head>
-  <title>Launchpad lp.client moduel</title>
+  <title>Launchpad lp.client module</title>
 
   <!-- YUI and test setup -->
   <script type="text/javascript"
@@ -10,8 +10,11 @@
   <link rel="stylesheet" href="../../../app/javascript/testing/test.css" />
   <script type="text/javascript"
           src="../../../app/javascript/testing/testrunner.js"></script>
+  <script type="text/javascript"
+          src="../../../app/javascript/testing/iorecorder.js"></script>
 
   <!-- The module under test -->
+  <script type="text/javascript" src="../lp.js"></script>
   <script type="text/javascript" src="../client.js"></script>
 
   <!-- The test suite -->

=== modified file 'lib/lp/app/javascript/tests/test_lp_client.js'
--- lib/lp/app/javascript/tests/test_lp_client.js	2011-07-08 05:12:39 +0000
+++ lib/lp/app/javascript/tests/test_lp_client.js	2011-07-27 18:39:38 +0000
@@ -1,7 +1,7 @@
 /* Copyright (c) 2011, Canonical Ltd. All rights reserved. */
 
-YUI().use('lp.testing.runner', 'test', 'console', 'lp.client',
-          'escape', function(Y) {
+YUI().use('lp.testing.runner', 'lp.testing.iorecorder', 'test', 'console',
+          'lp.client', 'escape', function(Y) {
 
 var Assert = Y.Assert;  // For easy access to isTrue(), etc.
 
@@ -58,6 +58,77 @@
       Assert.areEqual(
           get_field_uri("/has/slash/", "field"),
           "/api/devel/has/slash/field");
+    },
+    test_view_url: function() {
+        entry_repr = {web_link: 'http://example.com/context'};
+        var context = new Y.lp.client.Entry(null, entry_repr, null);
+        expected = '/context/+myview/++mynamespace++';
+        actual = Y.lp.client.get_view_url(context, '+myview', 'mynamespace');
+        Assert.areEqual(expected, actual);
+    },
+    test_get_form_url: function() {
+        entry_repr = {web_link: 'http://example.com/context'};
+        var context = new Y.lp.client.Entry(null, entry_repr, null);
+        expected = '/context/+myview/++form++';
+        actual = Y.lp.client.get_form_url(context, '+myview');
+        Assert.areEqual(expected, actual);
+    },
+    test_load_model: function(){
+        var recorder = new Y.lp.testing.iorecorder.IORecorder();
+        Assert.areEqual(0, recorder.requests.length);
+        var mylist = [];
+        var config = {
+            io: Y.bind(recorder.do_io, recorder),
+            on: {
+                success: Y.bind(mylist.push, mylist)
+            }
+        };
+        var entry_repr = {web_link: 'http://example.com/context'};
+        var client = new Y.lp.client.Launchpad();
+        var context = new Y.lp.client.Entry(client, entry_repr, null);
+        Y.lp.client.load_model(context, '+myview', config);
+        var request = recorder.requests[0];
+        Assert.areEqual('/context/+myview/++model++', request.url);
+        request.success(
+            '{"boolean": true, "entry": {"resource_type_link": "foo"}}',
+            {'Content-Type': 'application/json'});
+        var result = mylist[0];
+        Assert.areSame(true, result.boolean);
+        Assert.isInstanceOf(Y.lp.client.Entry, result.entry);
+        Assert.areSame('foo', result.entry.get('resource_type_link'));
+    },
+    test_wrap_resource_nested_mapping: function() {
+        var foo = {
+            baa: {},
+            bar: {
+                baz: {
+                    resource_type_link: 'qux'}
+            }
+        };
+        foo = new Y.lp.client.Launchpad().wrap_resource(null, foo);
+        Y.Assert.isInstanceOf(Y.lp.client.Entry, foo.bar.baz);
+    },
+    test_wrap_resource_nested_array: function() {
+        var foo = [[{resource_type_link: 'qux'}]];
+        foo = new Y.lp.client.Launchpad().wrap_resource(null, foo);
+        Y.Assert.isInstanceOf(Y.lp.client.Entry, foo[0][0]);
+    },
+    test_wrap_resource_creates_array: function() {
+        var foo = ['a'];
+        var bar = new Y.lp.client.Launchpad().wrap_resource(null, foo);
+        Y.Assert.areNotSame(foo, bar);
+    },
+    test_wrap_resource_creates_mapping: function() {
+        var foo = {a: 'b'};
+        var bar = new Y.lp.client.Launchpad().wrap_resource(null, foo);
+        Y.Assert.areNotSame(foo, bar);
+    },
+    test_wrap_resource_null: function() {
+        var foo = {
+            bar: null
+        };
+        foo = new Y.lp.client.Launchpad().wrap_resource(null, foo);
+        Y.Assert.isNull(foo.bar);
     }
 }));
 
@@ -208,28 +279,13 @@
       }
 }));
 
-function MockHttpResponse () {
-    this.responseText = '[]';
-    this.responseHeaders = {};
-}
-
-MockHttpResponse.prototype = {
-    setResponseHeader: function (header, value) {
-        this.responseHeaders[header] = value;
-    },
-
-    getResponseHeader: function(header) {
-        return this.responseHeaders[header];
-    }
-};
-
 suite.add(new Y.Test.Case({
     name: "lp.client.notifications",
 
     setUp: function() {
         this.client = new Y.lp.client.Launchpad();
         this.args=[this.client, null, this._on_success, false];
-        this.response = new MockHttpResponse();
+        this.response = new Y.lp.testing.iorecorder.MockHttpResponse();
         this.response.setResponseHeader('Content-Type', 'application/json');
     },
 

=== modified file 'lib/lp/translations/javascript/sourcepackage_sharing_details.js'
--- lib/lp/translations/javascript/sourcepackage_sharing_details.js	2011-04-22 21:33:30 +0000
+++ lib/lp/translations/javascript/sourcepackage_sharing_details.js	2011-07-27 18:39:38 +0000
@@ -83,21 +83,6 @@
 });
 
 namespace.LinkCheckItem = LinkCheckItem;
-namespace.convert_cache = function(lp_client, cache) {
-    var new_cache = {};
-    var value;
-    var key;
-    for (key in cache){
-        if (cache.hasOwnProperty(key)){
-            value = cache[key];
-            if (value !== null){
-                value = lp_client.wrap_resource(value.self_link, value);
-            }
-            new_cache[key] = value;
-        }
-    }
-    return new_cache;
-};
 
 
 namespace.autoimport_modes = {
@@ -174,14 +159,8 @@
 }
 
 
-function form_url(entry, view_name) {
-    entry_url = Y.lp.get_url_path(entry.get('web_link'));
-    return entry_url + '/' + view_name + '/++form++';
-}
-
-
 function update_form(overlay, entry, view_name) {
-    var url = form_url(entry, view_name);
+    var url = Y.lp.client.get_form_url(entry, view_name);
     overlay.loadFormContentAndRender(url);
     overlay.render();
 }
@@ -212,7 +191,7 @@
     }
     form_data_entries.push('field.actions.' + action + '=ignored');
     var data = form_data_entries.join('&');
-    var url = form_url(entry, view_name);
+    var url = Y.lp.client.get_form_url(entry, view_name);
     config.method = 'POST';
     config.data = data;
     Y.io(url, config);
@@ -583,7 +562,7 @@
 namespace.prepare = function(cache) {
     var sharing_controller = new namespace.TranslationSharingController();
     var lp_client = new Y.lp.client.Launchpad();
-    cache = namespace.convert_cache(lp_client, cache);
+    cache = lp_client.wrap_resource(null, cache);
     var branch_picker_config = {
         picker_activator: '#branch-incomplete-picker a',
         header : 'Select translation branch',

=== modified file 'lib/lp/translations/javascript/tests/test_sourcepackage_sharing_details.js'
--- lib/lp/translations/javascript/tests/test_sourcepackage_sharing_details.js	2011-07-08 06:21:11 +0000
+++ lib/lp/translations/javascript/tests/test_sourcepackage_sharing_details.js	2011-07-27 18:39:38 +0000
@@ -133,7 +133,7 @@
             };
             var ctrl = new TranslationSharingController();
             var lp_client = new Y.lp.client.Launchpad();
-            cache = test_ns.convert_cache(lp_client, cache);
+            cache = lp_client.wrap_resource(null, cache);
             var import_overlay = {
                 loadFormContentAndRender: function() {},
                 render: function() {},
@@ -159,21 +159,6 @@
                 tsconfig.get('translations_usage').get('complete'));
             Y.Assert.areSame(ctrl.get('source_package'), cache.context);
         },
-        test_convert_cache: function() {
-            var cache = {
-                foo: {
-                    self_link: 'http://foo',
-                    resource_type_link: 'http://foo_type'
-                },
-                bar: null,
-                baz: true
-            };
-            var lp_client = new Y.lp.client.Launchpad();
-            cache = test_ns.convert_cache(lp_client, cache);
-            Y.Assert.isNull(cache.bar);
-            Y.Assert.isTrue(cache.baz);
-            Y.Assert.areEqual('http://foo', cache.foo.get('self_link'));
-        },
         test_set_permissions: function(){
             var ctrl = new TranslationSharingController();
             var config = ctrl.get('tsconfig');