← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~abentley/launchpad/update-listings into lp:launchpad

 

Aaron Bentley has proposed merging lp:~abentley/launchpad/update-listings into lp:launchpad with lp:~abentley/launchpad/mustache-bugs as a prerequisite.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~abentley/launchpad/update-listings/+merge/79998

= Summary =
Implement dynamic reordering of bug listings.  Part of: https://dev.launchpad.net/Projects/CustomBugListings

== Proposed fix ==
Use the ++model++ to retrieve listings with an updated ordering.  Preserve all other query parameters.

== Pre-implementation notes ==

== Implementation details ==
Implemented get_query to retrieve the current query in structured form. Updated load_model to accept query parameters.  Implemented update_listing to render a specified ordering.

Changed buglisting-default.pt so that the default rendering is temporarily forced to 'status'.  (This is only needed until the ordering widget is implemented.)

== Tests ==
xvfb-run bin/test -t test_buglisting.html

== Demo and Q/A ==
Enable the feature flag.  Go to a bug listing.  The 'client-side mustache' listing should show the bugs ordered by status, whatever specified order.  Regardless of order, it should show the same bugs.


= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/bugs/javascript/tests/test_buglisting.html
  lib/lp/app/javascript/lp.js
  lib/lp/bugs/javascript/buglisting.js
  lib/lp/app/javascript/testing/assert.js
  lib/lp/app/javascript/client.js
  lib/lp/bugs/templates/buglisting-default.pt
  lib/lp/bugs/browser/tests/test_bugtask.py
  lib/lp/bugs/browser/bugtask.py
  lib/lp/bugs/templates/buglisting.mustache
  lib/lp/app/javascript/testing/mockio.js
  lib/lp/bugs/templates/bugs-listing-table-without-navlinks.pt
  lib/lp/bugs/javascript/tests/test_buglisting.js

./lib/lp/app/javascript/lp.js
      52: Expected '!==' and instead saw '!='.
     145: Don't make functions within a loop.
     154: Expected '===' and instead saw '=='.
     171: Expected '===' and instead saw '=='.
     171: Expected '===' and instead saw '=='.
     183: Expected '===' and instead saw '=='.
     213: Expected '===' and instead saw '=='.
-- 
https://code.launchpad.net/~abentley/launchpad/update-listings/+merge/79998
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~abentley/launchpad/update-listings into lp:launchpad.
=== modified file 'lib/lp/app/javascript/client.js'
--- lib/lp/app/javascript/client.js	2011-10-11 17:05:30 +0000
+++ lib/lp/app/javascript/client.js	2011-10-20 20:30:31 +0000
@@ -190,11 +190,17 @@
  * @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){
+module.get_view_url = function(entry, view_name, namespace, query){
     entry_url = Y.lp.get_url_path(entry.get('web_link'));
-    return entry_url + '/' + view_name + '/++' + namespace + '++';
+    querystring = Y.QueryString.stringify(query);
+    if (querystring !== '') {
+        querystring = '?' + querystring;
+    }
+    return (
+        entry_url + '/' + view_name + '/++' + namespace + '++' + querystring);
 };
 
 
@@ -210,8 +216,16 @@
 };
 
 
-module.load_model = function(entry, view_name, config){
-    var url = module.get_view_url(entry, view_name, 'model');
+/**
+ * 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;
@@ -936,7 +950,8 @@
 
 
 }, "0.1",
-    {"requires":["attribute", "io", "json-parse", "json-stringify", "lp"]});
+    {"requires":["attribute", "io", "querystring", "json-parse",
+                 "json-stringify", "lp"]});
 
 
 YUI.add('lp.client.plugins', function (Y) {

=== modified file 'lib/lp/app/javascript/lp.js'
--- lib/lp/app/javascript/lp.js	2011-07-18 09:23:10 +0000
+++ lib/lp/app/javascript/lp.js	2011-10-20 20:30:31 +0000
@@ -64,10 +64,34 @@
             '.collapsible', 'legend', 'legend + *', true);
     };
 
+    /**
+     * Return a hyperlink with the specified URL.
+     */
+    var get_hyperlink  = function(url){
+        var link =  Y.Node.create('<a>junk</a>');
+        link.set('href', url);
+        return link;
+    };
+
+    /**
+     * Return the path portion of the specified URL.
+     */
     Y.lp.get_url_path = function(url) {
-         return Y.Node.create('<a>junk</a>').set('href', url).get('pathname');
-    };
-}, "0.1", {"requires":["cookie", "lazr.effects", "lp.app.widgets.expander"]});
+        return get_hyperlink(url).get('pathname');
+    };
+
+    /**
+     * Return the query string of the specified URL.
+     */
+    Y.lp.get_url_query = function(url){
+        var link = get_hyperlink(url);
+        var query = link.get('search');
+        if (query.length > 0) {
+            query = query.slice(1);
+        }
+        return query;
+    };
+}, "0.1", {"requires":["cookie", "lp.app.widgets.expander"]});
 
 
 // Lint-safe scripting URL.
@@ -105,8 +129,9 @@
 function setFocusByName(name) {
     // Focus the first element matching the given name which can be focused.
     var nodes = document.getElementsByName(name);
-    for (var i = 0; i < nodes.length; i++) {
-        var node = nodes[i];
+    var i, node;
+    for (i = 0; i < nodes.length; i++) {
+        node = nodes[i];
         if (node.focus) {
             try {
                 // Trying to focus a hidden element throws an error in IE8.

=== added file 'lib/lp/app/javascript/testing/assert.js'
--- lib/lp/app/javascript/testing/assert.js	1970-01-01 00:00:00 +0000
+++ lib/lp/app/javascript/testing/assert.js	2011-10-20 20:30:31 +0000
@@ -0,0 +1,17 @@
+/* Copyright (c) 2011, Canonical Ltd. All rights reserved. */
+
+YUI.add('lp.testing.assert', function(Y) {
+    /**
+     * A utility module for use in YUI unit-tests with custom asserts
+     *
+     * @module lp.testing.mockio
+     */
+    var namespace =  Y.namespace("lp.testing.assert");
+
+    /**
+     * Assert that two structures are equal by comparing their json form.
+     */
+    namespace.assert_equal_structure = function(expected, actual){
+        Y.Assert.areEqual(JSON.stringify(expected), JSON.stringify(actual));
+    };
+}, '0.1', {'requires': []});

=== modified file 'lib/lp/app/javascript/testing/mockio.js'
--- lib/lp/app/javascript/testing/mockio.js	2011-08-24 15:41:23 +0000
+++ lib/lp/app/javascript/testing/mockio.js	2011-10-20 20:30:31 +0000
@@ -82,6 +82,20 @@
         }
     };
 
+    /**
+     * Provide a successful JSON response
+     *
+     * @method successJSON
+     * @param {Object} json_data JSON-serializable data to provide as the
+     * response.
+     */
+    MockHttpRequest.prototype.successJSON = function(json_data){
+        this.respond({
+            responseText: Y.JSON.stringify(json_data),
+            responseHeaders: {'Content-Type': 'application/json'}
+        });
+    };
+
     namespace.MockHttpRequest = MockHttpRequest;
 
     var MockIo = function() {

=== modified file 'lib/lp/bugs/javascript/buglisting.js'
--- lib/lp/bugs/javascript/buglisting.js	2011-10-20 20:30:31 +0000
+++ lib/lp/bugs/javascript/buglisting.js	2011-10-20 20:30:31 +0000
@@ -11,13 +11,66 @@
 
 var namespace = Y.namespace('lp.bugs.buglisting');
 
-namespace.rendertable = function(){
+
+/**
+ * Render bug listings via Mustache.
+ *
+ * If model is supplied, it is used as the data for rendering the listings.
+ * Otherwise, LP.cache.mustache_model is used.
+ *
+ * The template is always LP.mustache_listings.
+ */
+namespace.rendertable = function(model){
     client_listing = Y.one('#client-listing');
     if (client_listing === null){
         return;
     }
-    var txt = Mustache.to_html(LP.mustache_listings, LP.cache.mustache_model);
+    if (!Y.Lang.isValue(model)){
+        model = LP.cache.mustache_model;
+    }
+    var txt = Mustache.to_html(LP.mustache_listings, model);
     client_listing.set('innerHTML', txt);
 };
 
-}, "0.1", {"requires": ["node"]});
+/**
+ * A shim to use the data of an LP.cache to render the bug listings.
+ */
+namespace.update_from_model = function(model){
+    namespace.rendertable(model.mustache_model);
+};
+
+
+/**
+ * Update the bug listings.
+ *
+ * order_by is a string specifying the sort order, as it would appear in a
+ * URL.
+ */
+namespace.update_listing = function(order_by){
+    var lp_client = new Y.lp.client.Launchpad();
+    var cache = lp_client.wrap_resource(null, LP.cache);
+    var query = namespace.get_query(window.location);
+    query.orderby = order_by;
+    load_model_config = {
+        on: {
+            success: namespace.update_from_model
+        }
+    };
+    if (Y.Lang.isValue(config)){
+        load_model_config.io_provider = config.io_provider;
+    }
+    Y.lp.client.load_model(
+        cache.context, '+bugs', load_model_config, query);
+};
+
+
+/**
+ * Return the query of the specified URL in structured form.
+ */
+namespace.get_query = function(url){
+    var querystring = Y.lp.get_url_query(url);
+    return Y.QueryString.parse(querystring);
+};
+
+
+}, "0.1", {"requires": ["node", 'lp.client']});

=== modified file 'lib/lp/bugs/javascript/tests/test_buglisting.html'
--- lib/lp/bugs/javascript/tests/test_buglisting.html	2011-10-20 20:30:31 +0000
+++ lib/lp/bugs/javascript/tests/test_buglisting.html	2011-10-20 20:30:31 +0000
@@ -9,6 +9,18 @@
   <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/assert.js"></script>
+  <script type="text/javascript"
+          src="../../../app/javascript/testing/mockio.js"></script>
+  <script type="text/javascript"
+          src="../../../app/javascript/effects/effects.js"></script>
+  <script type="text/javascript"
+          src="../../../app/javascript/expander.js"></script>
+  <script type="text/javascript"
+          src="../../../app/javascript/lp.js"></script>
+  <script type="text/javascript"
+          src="../../../app/javascript/client.js"></script>
 
     <script type="text/javascript"
       src="../../../contrib/javascript/mustache.js"></script>

=== modified file 'lib/lp/bugs/javascript/tests/test_buglisting.js'
--- lib/lp/bugs/javascript/tests/test_buglisting.js	2011-10-20 20:30:31 +0000
+++ lib/lp/bugs/javascript/tests/test_buglisting.js	2011-10-20 20:30:31 +0000
@@ -1,15 +1,21 @@
 YUI({
     base: '../../../../canonical/launchpad/icing/yui/',
     filter: 'raw', combine: false, fetchCSS: false
-    }).use('test', 'console', 'lp.bugs.buglisting', function(Y) {
+    }).use('test', 'console', 'lp.bugs.buglisting', 'lp.testing.mockio',
+           'lp.testing.assert',
+           function(Y) {
 
 var suite = new Y.Test.Suite("lp.bugs.buglisting Tests");
 var module = Y.lp.bugs.buglisting;
 
 /**
- * Test is_notification_level_shown() for a given set of
- * conditions.
+ * Set the HTML fixture to the desired value.
  */
+var set_fixture = function(value){
+    var fixture = Y.one('#fixture');
+    fixture.set('innerHTML', value);
+};
+
 suite.add(new Y.Test.Case({
     name: 'rendertable',
 
@@ -20,18 +26,16 @@
 
     tearDown: function() {
         delete window.LP;
-        this.set_fixture('');
+        set_fixture('');
     },
 
-    set_fixture: function(value) {
-        var fixture = Y.one('#fixture');
-        fixture.set('innerHTML', value);
-    },
     test_rendertable_no_client_listing: function() {
+        // Rendering should not error with no #client-listing.
         module.rendertable();
     },
     test_rendertable: function() {
-        this.set_fixture('<div id="client-listing"></div>');
+        // Rendering should work with #client-listing supplied.
+        set_fixture('<div id="client-listing"></div>');
         window.LP.cache = {
             mustache_model: {
                 foo: 'bar'
@@ -43,6 +47,65 @@
     }
 }));
 
+suite.add(new Y.Test.Case({
+    name: 'update_listing',
+
+    setUp: function () {
+        this.MY_NAME = "ME";
+        var lp_client = new Y.lp.client.Launchpad();
+        window.LP = { links: { me: "/~" + this.MY_NAME } };
+        set_fixture('<div id="client-listing"></div>');
+        window.LP.cache = {
+            context: {
+                resource_type_link: 'http://foo_type',
+                web_link: 'http://foo/bar'
+            },
+            mustache_model: {
+                foo: 'bar'
+            }
+        };
+        window.LP.mustache_listings = "<ol>" +
+            "{{#item}}<li>{{name}}</li>{{/item}}</ol>";
+    },
+    tearDown: function() {
+        delete window.LP;
+    },
+    test_update_listing: function() {
+        /* update_listing retrieves a listing for the new ordering and
+         * displays it */
+        mock_io = new Y.lp.testing.mockio.MockIo();
+        module.update_listing('intensity', {io_provider: mock_io});
+        Y.Assert.areEqual('',
+            Y.one('#client-listing').get('innerHTML'));
+        Y.Assert.areEqual('/bar/+bugs/++model++?orderby=intensity',
+            mock_io.last_request.url);
+        mock_io.last_request.successJSON({mustache_model:
+            {item: [
+                {name: 'first'},
+                {name: 'second'}
+            ]}
+        });
+        Y.Assert.areEqual('<ol><li>first</li><li>second</li></ol>',
+            Y.one('#client-listing').get('innerHTML'));
+    }
+}));
+
+suite.add(new Y.Test.Case({
+    name: 'get_query',
+
+    setUp: function () {
+    },
+    tearDown: function() {
+        delete window.LP;
+    },
+    test_get_query: function() {
+        // get_query returns the query portion of a URL in structured form.
+        var query = module.get_query('http://yahoo.com?me=you&a=b&a=c');
+        Y.lp.testing.assert.assert_equal_structure(
+            {me: 'you', a: ['b', 'c']}, query);
+    }
+}));
+
 var handle_complete = function(data) {
     window.status = '::::' + JSON.stringify(data);
     };

=== modified file 'lib/lp/bugs/templates/buglisting-default.pt'
--- lib/lp/bugs/templates/buglisting-default.pt	2011-10-20 20:30:31 +0000
+++ lib/lp/bugs/templates/buglisting-default.pt	2011-10-20 20:30:31 +0000
@@ -23,7 +23,7 @@
         Y.on('domready', function() {
             Y.lp.registry.structural_subscription.setup(
                 {content_box: "#structural-subscription-content-box"});
-            Y.lp.bugs.buglisting.rendertable()
+            Y.lp.bugs.buglisting.update_listing('status');
         });
     });
   </script>