launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #05325
[Merge] lp:~abentley/launchpad/navigate-batches into lp:launchpad
Aaron Bentley has proposed merging lp:~abentley/launchpad/navigate-batches into lp:launchpad.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~abentley/launchpad/navigate-batches/+merge/80502
= Summary =
Support AJAX-powered navigation, with caching.
== Proposed fix ==
Unify existing functionality into new ListingNavigator type. Introduce methods for navigating to the next, previous, first and last batches. Update the cache to store batches with four-value keys: memo, start, forwards and ordering.
== Pre-implementation notes ==
None
== Implementation details ==
Since Javascript does not provide tuples as an option for mapping keys, the values are serialized as a list.
The test fixtures were removed, because they can now be handled by suppling specific data to the ListingNavigator's constructor.
The mustache-based listings now override the TAL listings. The server-side rendering is used initially, but replaced with the client-side rendering when AJAX funcitonality is used.
== Tests ==
bin/test -t test_buglisting.html -t test_bugtask
== Demo and Q/A ==
These listings are only enabled on qastaging, and only for Orange squad.
Navigating to the first, next, previous, and last pages of results should work.
Navigating to the last page and then to the previous page should work.
Navigation should respect the search filters.
Caching should be enabled, but only works when a batch is reached through the same link. So if you do "Next, Previous, Next", the second Next will be retrieved from cache, but not Previous.
The "1 of 501 results" text will not update. Links that are shown as disabled will will work when appropriate. There's more to do, but the diff is already big.
= Launchpad lint =
Checking for conflicts and issues in changed files.
Linting changed files:
lib/lp/bugs/javascript/buglisting.js
lib/lp/bugs/templates/bugs-listing-table.pt
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/bugs-listing-table-without-navlinks.pt
lib/lp/bugs/javascript/tests/test_buglisting.js
--
https://code.launchpad.net/~abentley/launchpad/navigate-batches/+merge/80502
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~abentley/launchpad/navigate-batches into lp:launchpad.
=== modified file 'lib/lp/bugs/browser/bugtask.py'
--- lib/lp/bugs/browser/bugtask.py 2011-10-20 19:14:32 +0000
+++ lib/lp/bugs/browser/bugtask.py 2011-10-26 20:19:27 +0000
@@ -2212,7 +2212,10 @@
@property
def mustache(self):
- return pystache.render(self.mustache_template, self.model)
+ """The rendered mustache template."""
+ cache = IJSONRequestCache(self.request)
+ return pystache.render(self.mustache_template,
+ cache.objects['mustache_model'])
@property
def model(self):
@@ -2479,7 +2482,25 @@
self.context, self.request, self.user)
if getFeatureFlag('bugs.dynamic_bug_listings.enabled'):
cache = IJSONRequestCache(self.request)
- cache.objects['mustache_model'] = self.search().model
+ batch_navigator = self.search()
+ cache.objects['mustache_model'] = batch_navigator.model
+
+ def _getBatchInfo(batch):
+ if batch is None:
+ return None
+ return {'memo': batch.range_memo,
+ 'start': batch.startNumber() - 1}
+
+ next_batch = batch_navigator.batch.nextBatch()
+ cache.objects['next'] = _getBatchInfo(next_batch)
+ prev_batch = batch_navigator.batch.prevBatch()
+ cache.objects['prev'] = _getBatchInfo(prev_batch)
+ cache.objects['order_by'] = ','.join(
+ get_sortorder_from_request(self.request))
+ cache.objects['forwards'] = batch_navigator.batch.range_forwards
+ last_batch = batch_navigator.batch.lastBatch()
+ cache.objects['last_start'] = last_batch.startNumber() - 1
+ cache.objects.update(_getBatchInfo(batch_navigator.batch))
@property
def columns_to_show(self):
=== modified file 'lib/lp/bugs/browser/tests/test_bugtask.py'
--- lib/lp/bugs/browser/tests/test_bugtask.py 2011-10-20 17:46:49 +0000
+++ lib/lp/bugs/browser/tests/test_bugtask.py 2011-10-26 20:19:27 +0000
@@ -6,6 +6,7 @@
from contextlib import contextmanager
from datetime import datetime
import re
+import urllib
from lazr.lifecycle.event import ObjectModifiedEvent
from lazr.restful.interfaces import IJSONRequestCache
@@ -1229,14 +1230,35 @@
layer = DatabaseFunctionalLayer
- server_listing = soupmatchers.Tag(
- 'Server', 'em', text='Server-side mustache')
-
client_listing = soupmatchers.Tag(
'client-listing', True, attrs={'id': 'client-listing'})
- def makeView(self, bugtask=None):
- request = LaunchpadTestRequest()
+ def makeView(self, bugtask=None, size=None, memo=None, orderby=None,
+ forwards=True):
+ """Make a BugTaskSearchListingView.
+
+ :param bugtask: The task to use for searching.
+ :param size: The size of the batches. Required if forwards is False.
+ :param memo: Batch identifier.
+ :param orderby: The way to order the batch.
+ :param forwards: If true, walk forwards from the memo. Else walk
+ backwards.
+
+ """
+ query_vars = {}
+ if size is not None:
+ query_vars['batch'] = size
+ if memo is not None:
+ query_vars['memo'] = memo
+ if forwards:
+ query_vars['start'] = memo
+ else:
+ query_vars['start'] = int(memo) - size
+ if not forwards:
+ query_vars['direction'] = 'backwards'
+ query_string = urllib.urlencode(query_vars)
+ request = LaunchpadTestRequest(
+ QUERY_STRING=query_string, orderby=orderby)
if bugtask is None:
bugtask = self.factory.makeBugTask()
view = BugTaskSearchListingView(bugtask.target, request)
@@ -1245,6 +1267,7 @@
@contextmanager
def dynamic_listings(self):
+ """Context manager to enable new bug listings."""
with feature_flags():
set_feature_flag(u'bugs.dynamic_bug_listings.enabled', u'on')
yield
@@ -1270,7 +1293,92 @@
self.assertEqual(1, len(bugtasks))
self.assertEqual(item.model, bugtasks[0])
+ def test_no_next_prev_for_single_batch(self):
+ """The IJSONRequestCache should contain data about ajacent batches.
+
+ mustache_model should contain bugtasks, the BugTaskListingItem.model
+ for each BugTask.
+ """
+ owner, item = make_bug_task_listing_item(self.factory)
+ self.useContext(person_logged_in(owner))
+ with self.dynamic_listings():
+ view = self.makeView(item.bugtask)
+ cache = IJSONRequestCache(view.request)
+ self.assertIs(None, cache.objects.get('next'))
+ self.assertIs(None, cache.objects.get('prev'))
+
+ def test_next_for_multiple_batch(self):
+ """The IJSONRequestCache should contain data about the next batch.
+
+ mustache_model should contain bugtasks, the BugTaskListingItem.model
+ for each BugTask.
+ """
+ task = self.factory.makeBugTask()
+ self.factory.makeBugTask(target=task.target)
+ with self.dynamic_listings():
+ view = self.makeView(task, size=1)
+ cache = IJSONRequestCache(view.request)
+ self.assertEqual({'memo': '1', 'start': 1}, cache.objects.get('next'))
+
+ def test_prev_for_multiple_batch(self):
+ """The IJSONRequestCache should contain data about the next batch.
+
+ mustache_model should contain bugtasks, the BugTaskListingItem.model
+ for each BugTask.
+ """
+ task = self.factory.makeBugTask()
+ task2 = self.factory.makeBugTask(target=task.target)
+ with self.dynamic_listings():
+ view = self.makeView(task2, size=1, memo=1)
+ cache = IJSONRequestCache(view.request)
+ self.assertEqual({'memo': '1', 'start': 0}, cache.objects.get('prev'))
+
+ def test_default_order_by(self):
+ """order_by defaults to '-importance in JSONRequestCache"""
+ task = self.factory.makeBugTask()
+ with self.dynamic_listings():
+ view = self.makeView(task)
+ cache = IJSONRequestCache(view.request)
+ self.assertEqual('-importance', cache.objects['order_by'])
+
+ def test_order_by_importance(self):
+ """order_by follows query params in JSONRequestCache"""
+ task = self.factory.makeBugTask()
+ with self.dynamic_listings():
+ view = self.makeView(task, orderby='importance')
+ cache = IJSONRequestCache(view.request)
+ self.assertEqual('importance', cache.objects['order_by'])
+
+ def test_cache_has_all_batch_vars_defaults(self):
+ """Cache has all the needed variables.
+
+ order_by, memo, start, forwards. These default to sane values.
+ """
+ task = self.factory.makeBugTask()
+ with self.dynamic_listings():
+ view = self.makeView(task)
+ cache = IJSONRequestCache(view.request)
+ self.assertEqual('-importance', cache.objects['order_by'])
+ self.assertIs(None, cache.objects['memo'])
+ self.assertEqual(0, cache.objects['start'])
+ self.assertTrue(cache.objects['forwards'])
+
+ def test_cache_has_all_batch_vars_specified(self):
+ """Cache has all the needed variables.
+
+ order_by, memo, start, forwards. These are calculated appropriately.
+ """
+ task = self.factory.makeBugTask()
+ with self.dynamic_listings():
+ view = self.makeView(task, memo=1, forwards=False, size=1)
+ cache = IJSONRequestCache(view.request)
+ self.assertEqual('1', cache.objects['memo'])
+ self.assertEqual(0, cache.objects['start'])
+ self.assertFalse(cache.objects['forwards'])
+ self.assertEqual(0, cache.objects['last_start'])
+
def getBugtaskBrowser(self):
+ """Return a browser for a new bugtask."""
bugtask = self.factory.makeBugTask()
with person_logged_in(bugtask.target.owner):
bugtask.target.official_malone = True
@@ -1279,6 +1387,7 @@
return bugtask, browser
def assertHTML(self, browser, *tags, **kwargs):
+ """Assert something about a browser's HTML."""
matcher = soupmatchers.HTMLContains(*tags)
if kwargs.get('invert', False):
matcher = Not(matcher)
@@ -1296,15 +1405,13 @@
number_tag = self.getBugNumberTag(bug_task)
self.assertHTML(browser, number_tag, invert=True)
self.assertHTML(browser, self.client_listing, invert=True)
- self.assertHTML(browser, self.server_listing, invert=True)
def test_mustache_rendering(self):
"""If the flag is present, then all mustache features appear."""
with self.dynamic_listings():
bug_task, browser = self.getBugtaskBrowser()
bug_number = self.getBugNumberTag(bug_task)
- self.assertHTML(
- browser, self.client_listing, self.server_listing, bug_number)
+ self.assertHTML(browser, self.client_listing, bug_number)
class TestBugTaskListingItem(TestCaseWithFactory):
=== modified file 'lib/lp/bugs/javascript/buglisting.js'
--- lib/lp/bugs/javascript/buglisting.js 2011-10-20 20:33:40 +0000
+++ lib/lp/bugs/javascript/buglisting.js 2011-10-26 20:19:27 +0000
@@ -13,6 +13,63 @@
/**
+ * Constructor.
+ * current_url is used to determine search params.
+ * cache is the JSONRequestCache for the batch.
+ * template is the template to use for rendering batches.
+ * target is a YUI node to update when rendering batches.
+ * io_provider is something providing the Y.io interface, typically used for
+ * testing. Defaults to Y.io.
+ */
+namespace.ListingNavigator = function(current_url, cache, template, target,
+ io_provider) {
+ var lp_client = new Y.lp.client.Launchpad();
+ this.search_params = namespace.get_query(current_url);
+ delete this.search_params.start;
+ delete this.search_params.memo;
+ delete this.search_params.direction;
+ delete this.search_params.orderby;
+ this.io_provider = io_provider;
+ this.current_batch = lp_client.wrap_resource(null, cache);
+ this.template = template;
+ this.target = target;
+ this.batches = {};
+};
+
+
+/**
+ * Call the callback when a node matching the selector is clicked.
+ *
+ * The node is also marked up appropriately.
+ */
+namespace.ListingNavigator.prototype.clickAction = function(selector,
+ callback){
+ that = this;
+ var nodes = Y.all(selector);
+ nodes.on('click', function(e){
+ e.preventDefault();
+ callback.call(that);
+ });
+ nodes.addClass('js-action');
+};
+
+
+/**
+ * Factory to return a ListingNavigator for the given page.
+ */
+namespace.ListingNavigator.from_page = function(){
+ var target = Y.one('#client-listing');
+ var navigator = new namespace.ListingNavigator(
+ window.location, LP.cache, LP.mustache_listings, target);
+ navigator.clickAction('.first', navigator.first_batch);
+ navigator.clickAction('.next', navigator.next_batch);
+ navigator.clickAction('.previous', navigator.prev_batch);
+ navigator.clickAction('.last', navigator.last_batch);
+ return navigator;
+};
+
+
+/**
* Render bug listings via Mustache.
*
* If model is supplied, it is used as the data for rendering the listings.
@@ -20,16 +77,22 @@
*
* The template is always LP.mustache_listings.
*/
-namespace.rendertable = function(model){
- client_listing = Y.one('#client-listing');
- if (client_listing === null){
+namespace.ListingNavigator.prototype.rendertable = function(){
+ if (! Y.Lang.isValue(this.target)){
return;
}
- if (!Y.Lang.isValue(model)){
- model = LP.cache.mustache_model;
- }
- var txt = Mustache.to_html(LP.mustache_listings, model);
- client_listing.set('innerHTML', txt);
+ var model = this.current_batch.mustache_model;
+ var txt = Mustache.to_html(this.template, model);
+ this.target.set('innerHTML', txt);
+};
+
+
+/**
+ * Get the key for the specified batch, for use in the batches mapping.
+ */
+namespace.ListingNavigator.get_batch_key = function(config){
+ return JSON.stringify([config.order_by, config.memo, config.forwards,
+ config.start]);
};
/**
@@ -38,40 +101,123 @@
*
* order_by is the ordering used by the model.
*/
-namespace.update_from_model = function(order_by, model){
- namespace.batches[order_by] = model.mustache_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.
- *
- * Config may contain an io_provider.
- */
-namespace.update_listing = function(order_by, config){
- var lp_client, cache, query;
- if (Y.Lang.isValue(namespace.batches[order_by])){
- namespace.update_from_model(order_by, namespace.batches[order_by]);
- return;
- }
- lp_client = new Y.lp.client.Launchpad();
- cache = lp_client.wrap_resource(null, LP.cache);
- query = namespace.get_query(window.location);
- query.orderby = order_by;
- load_model_config = {
+namespace.ListingNavigator.prototype.update_from_model = function(model){
+ var key = namespace.ListingNavigator.get_batch_key(model);
+ this.batches[key] = model;
+ this.current_batch = model;
+ this.rendertable();
+};
+
+
+/**
+ * Return the query vars to use for the specified batch.
+ * This includes the search params and the batch selector.
+ */
+namespace.ListingNavigator.prototype.get_batch_query = function(config){
+ var query = Y.merge(this.search_params, {orderby: config.order_by});
+ if (Y.Lang.isValue(config.memo)){
+ query.memo = config.memo;
+ }
+ if (Y.Lang.isValue(config.start)){
+ query.start = config.start;
+ }
+ if (config.forwards !== undefined && !config.forwards){
+ query.direction = 'backwards';
+ }
+ return query;
+};
+
+
+/**
+ * Update the display to the specified batch.
+ *
+ * If the batch is cached, it will be used immediately. Otherwise, it will be
+ * retrieved and cached upon retrieval.
+ */
+namespace.ListingNavigator.prototype.update = function(config){
+ var key = namespace.ListingNavigator.get_batch_key(config);
+ var cached_batch = this.batches[key];
+ if (Y.Lang.isValue(cached_batch)){
+ this.current_batch = cached_batch;
+ this.rendertable();
+ }
+ else {
+ this.load_model(config);
+ }
+};
+
+
+/**
+ * Update the navigator to display the last batch.
+ */
+namespace.ListingNavigator.prototype.last_batch = function(){
+ this.update({
+ forwards: false,
+ memo: "",
+ start: this.current_batch.last_start,
+ order_by: this.current_batch.order_by
+ });
+};
+
+
+/**
+ * Update the navigator to display the first batch.
+ *
+ * The order_by defaults to the current ordering, but may be overridden.
+ */
+namespace.ListingNavigator.prototype.first_batch = function(order_by){
+ if (order_by === undefined){
+ order_by = this.current_batch.order_by;
+ }
+ this.update({
+ forwards: true,
+ memo: null,
+ start: 0,
+ order_by: order_by
+ });
+};
+
+
+/**
+ * Update the navigator to display the next batch.
+ */
+namespace.ListingNavigator.prototype.next_batch = function(){
+ this.update({
+ forwards: true,
+ memo: this.current_batch.next.memo,
+ start:this.current_batch.next.start,
+ order_by: this.current_batch.order_by
+ });
+};
+
+/**
+ * Update the navigator to display the previous batch.
+ */
+namespace.ListingNavigator.prototype.prev_batch = function(){
+ this.update({
+ forwards: false,
+ memo: this.current_batch.prev.memo,
+ start:this.current_batch.prev.start,
+ order_by: this.current_batch.order_by
+ });
+};
+
+
+/**
+ * Load the specified batch via ajax. Display & cache on load.
+ */
+namespace.ListingNavigator.prototype.load_model = function(config){
+ var query = this.get_batch_query(config);
+ var load_model_config = {
on: {
- success: Y.bind(namespace.update_from_model, window, order_by)
+ success: Y.bind(this.update_from_model, this)
}
};
- if (Y.Lang.isValue(config)){
- load_model_config.io_provider = config.io_provider;
+ if (Y.Lang.isValue(this.io_provider)){
+ load_model_config.io_provider = this.io_provider;
}
Y.lp.client.load_model(
- cache.context, '+bugs', load_model_config, query);
+ this.current_batch.context, '+bugs', load_model_config, query);
};
@@ -84,6 +230,4 @@
};
-namespace.batches = {};
-
}, "0.1", {"requires": ["node", 'lp.client']});
=== modified file 'lib/lp/bugs/javascript/tests/test_buglisting.js'
--- lib/lp/bugs/javascript/tests/test_buglisting.js 2011-10-20 20:36:39 +0000
+++ lib/lp/bugs/javascript/tests/test_buglisting.js 2011-10-26 20:19:27 +0000
@@ -8,144 +8,156 @@
var suite = new Y.Test.Suite("lp.bugs.buglisting Tests");
var module = Y.lp.bugs.buglisting;
-/**
- * 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: 'ListingNavigator',
+ test_sets_search_params: function(){
+ // search_parms includes all query values that don't control batching
+ var navigator = new module.ListingNavigator(
+ 'http://yahoo.com?foo=bar&start=1&memo=2&direction=3&orderby=4');
+ Y.lp.testing.assert.assert_equal_structure(
+ {foo: 'bar'}, navigator.search_params);
+ }
+}));
suite.add(new Y.Test.Case({
name: 'rendertable',
- setUp: function () {
- this.MY_NAME = "ME";
- window.LP = { links: { me: "/~" + this.MY_NAME } };
- },
-
- tearDown: function() {
- delete window.LP;
- set_fixture('');
- },
-
test_rendertable_no_client_listing: function() {
// Rendering should not error with no #client-listing.
- module.rendertable();
+ var navigator = new module.ListingNavigator();
+ navigator.rendertable();
},
test_rendertable: function() {
// Rendering should work with #client-listing supplied.
- set_fixture('<div id="client-listing"></div>');
- window.LP.cache = {
+ var target = Y.Node.create('<div id="client-listing"></div>');
+ var lp_cache = {
mustache_model: {
foo: 'bar'
}
};
- window.LP.mustache_listings = "{{foo}}";
- module.rendertable();
- Y.Assert.areEqual('bar', Y.one('#client-listing').get('innerHTML'));
+ var template = "{{foo}}";
+ var navigator = new module.ListingNavigator(
+ null, lp_cache, template, target);
+ navigator.rendertable();
+ Y.Assert.areEqual('bar', navigator.target.get('innerHTML'));
}
}));
suite.add(new Y.Test.Case({
- name: 'update_listing',
+ name: 'first_batch',
- 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;
- module.batches = {};
- },
+ /**
+ * Return a ListingNavigator ordered by 'intensity'
+ */
get_intensity_listing: function(){
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'));
- mock_io.last_request.successJSON({mustache_model:
+ lp_cache = {
+ context: {
+ resource_type_link: 'http://foo_type',
+ web_link: 'http://foo/bar'
+ },
+ mustache_model: {
+ foo: 'bar'
+ }
+ };
+ var target = Y.Node.create('<div id="client-listing"></div>');
+ var navigator = new module.ListingNavigator(
+ "http://yahoo.com?start=5&memo=6&direction=backwards",
+ lp_cache, "<ol>" + "{{#item}}<li>{{name}}</li>{{/item}}</ol>",
+ target, mock_io);
+ navigator.first_batch('intensity');
+ Y.Assert.areEqual('', navigator.target.get('innerHTML'));
+ mock_io.last_request.successJSON({
+ context: {
+ resource_type_link: 'http://foo_type',
+ web_link: 'http://foo/bar'
+ },
+ mustache_model:
{item: [
{name: 'first'},
{name: 'second'}
- ]}
+ ]},
+ order_by: 'intensity',
+ start: 0,
+ forwards: true,
+ memo: null
});
- return mock_io;
+ return navigator;
},
- test_update_listing: function() {
- /* update_listing retrieves a listing for the new ordering and
+ test_first_batch: function() {
+ /* first_batch retrieves a listing for the new ordering and
* displays it */
- mock_io = this.get_intensity_listing();
+ var navigator = this.get_intensity_listing();
+ var mock_io = navigator.io_provider;
Y.Assert.areEqual('<ol><li>first</li><li>second</li></ol>',
- Y.one('#client-listing').get('innerHTML'));
- Y.Assert.areEqual('/bar/+bugs/++model++?orderby=intensity',
+ navigator.target.get('innerHTML'));
+ Y.Assert.areEqual('/bar/+bugs/++model++?orderby=intensity&start=0',
mock_io.last_request.url);
},
- test_update_listing_uses_cache: function() {
- /* update_listing will use the cached value instead of making a second
- * AJAX request. */
- mock_io = this.get_intensity_listing();
- Y.Assert.areEqual(1, mock_io.requests.length);
- module.update_listing('intensity', {io_provider: mock_io});
- Y.Assert.areEqual(1, mock_io.requests.length);
+ test_first_batch_uses_cache: function() {
+ /* first_batch will use the cached value instead of making a
+ * second AJAX request. */
+ var navigator = this.get_intensity_listing();
+ Y.Assert.areEqual(1, navigator.io_provider.requests.length);
+ navigator.first_batch('intensity');
+ Y.Assert.areEqual(1, navigator.io_provider.requests.length);
}
}));
+
suite.add(new Y.Test.Case({
name: 'Batch caching',
- 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;
- module.batches = {};
- },
test_update_from_model_caches: function() {
/* update_from_model caches the settings in the module.batches. */
- module.update_from_model('intensity', {mustache_model: {item: [
+ var lp_cache = {
+ context: {
+ resource_type_link: 'http://foo_type',
+ web_link: 'http://foo/bar'
+ },
+ mustache_model: {
+ foo: 'bar'
+ }
+ };
+ var template = "<ol>" +
+ "{{#item}}<li>{{name}}</li>{{/item}}</ol>";
+ var navigator = new module.ListingNavigator(window.location, lp_cache,
+ template);
+ var key = module.ListingNavigator.get_batch_key({
+ order_by: "intensity",
+ memo: 'memo1',
+ forwards: true,
+ start: 5
+ });
+ var batch = {
+ order_by: 'intensity',
+ memo: 'memo1',
+ forwards: true,
+ start: 5,
+ mustache_model: {item: [
{name: 'first'},
{name: 'second'}
- ]}});
+ ]}};
+ navigator.update_from_model(batch);
Y.lp.testing.assert.assert_equal_structure(
- {item: [{name: 'first'}, {name: 'second'}]},
- module.batches.intensity);
+ batch, navigator.batches[key]);
+ },
+ /**
+ * get_batch_key returns a JSON-serialized list.
+ */
+ test_get_batch_key: function(){
+ var key = module.ListingNavigator.get_batch_key({
+ order_by: 'order_by1',
+ memo: 'memo1',
+ forwards: true,
+ start: 5});
+ Y.Assert.areSame('["order_by1","memo1",true,5]', key);
}
}));
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');
@@ -154,6 +166,141 @@
}
}));
+
+suite.add(new Y.Test.Case({
+ name: 'get_batch_url',
+
+ /**
+ * get_batch_query accepts the order_by param.
+ */
+ test_get_batch_query_orderby: function(){
+ var navigator = new module.ListingNavigator('?param=1');
+ var query = navigator.get_batch_query({order_by: 'importance'});
+ Y.Assert.areSame('importance', query.orderby);
+ Y.Assert.areSame(1, query.param);
+ },
+ /**
+ * get_batch_query accepts the memo param.
+ */
+ test_get_batch_query_memo: function(){
+ var navigator = new module.ListingNavigator('?param=foo');
+ var query = navigator.get_batch_query({memo: 'pi'});
+ Y.Assert.areSame('pi', query.memo);
+ Y.Assert.areSame('foo', query.param);
+ },
+ /**
+ * When memo is null, query.memo is undefined.
+ */
+ test_get_batch_null_memo: function(){
+ var navigator = new module.ListingNavigator('?memo=foo');
+ var query = navigator.get_batch_query({memo: null});
+ Y.Assert.areSame(undefined, query.memo);
+ },
+ /**
+ * If 'forwards' is true, direction does not appear.
+ */
+ test_get_batch_query_forwards: function(){
+ var navigator = new module.ListingNavigator(
+ '?param=pi&direction=backwards');
+ var query = navigator.get_batch_query({forwards: true});
+ Y.Assert.areSame('pi', query.param);
+ Y.Assert.areSame(undefined, query.direction);
+ },
+ /**
+ * If 'forwards' is false, direction is set to backwards.
+ */
+ test_get_batch_query_backwards: function(){
+ var navigator = new module.ListingNavigator('?param=pi');
+ var query = navigator.get_batch_query({forwards: false});
+ Y.Assert.areSame('pi', query.param);
+ Y.Assert.areSame('backwards', query.direction);
+ },
+ /**
+ * If start is provided, it overrides existing values.
+ */
+ test_get_batch_query_start: function(){
+ var navigator = new module.ListingNavigator('?start=pi');
+ var query = navigator.get_batch_query({});
+ Y.Assert.areSame(undefined, query.start);
+ query = navigator.get_batch_query({start: 1});
+ Y.Assert.areSame(1, query.start);
+ query = navigator.get_batch_query({start: null});
+ Y.lp.testing.assert.assert_equal_structure({}, query);
+ }
+}));
+
+var get_navigator = function(url){
+ var mock_io = new Y.lp.testing.mockio.MockIo();
+ lp_cache = {
+ context: {
+ resource_type_link: 'http://foo_type',
+ web_link: 'http://foo/bar'
+ },
+ next: {
+ memo: 467,
+ start: 500
+ },
+ prev: {
+ memo: 457,
+ start: 400
+ },
+ order_by: 'foo',
+ last_start: 23
+ };
+ return new module.ListingNavigator(url, lp_cache, null, null, mock_io);
+};
+
+suite.add(new Y.Test.Case({
+ name: 'navigation',
+ /**
+ * last_batch uses memo="", start=navigator.current_batch.last_start,
+ * direction=backwards, orderby=navigator.current_batch.order_by.
+ */
+ test_last_batch: function(){
+ var navigator = get_navigator(
+ '?memo=pi&direction=backwards&start=57');
+ navigator.last_batch();
+ Y.Assert.areSame(
+ '/bar/+bugs/++model++?orderby=foo&memo=&start=23&' +
+ 'direction=backwards',
+ navigator.io_provider.last_request.url);
+ },
+ /**
+ * first_batch omits memo and direction, start=0,
+ * orderby=navigator.current_batch.order_by.
+ */
+ test_first_batch: function(){
+ var navigator = get_navigator('?memo=pi&start=26');
+ navigator.first_batch();
+ Y.Assert.areSame(
+ '/bar/+bugs/++model++?orderby=foo&start=0',
+ navigator.io_provider.last_request.url);
+ },
+ /**
+ * next_batch uses values from current_batch.next +
+ * current_batch.ordering.
+ */
+ test_next_batch: function(){
+ var navigator = get_navigator('?memo=pi&start=26');
+ navigator.next_batch();
+ Y.Assert.areSame(
+ '/bar/+bugs/++model++?orderby=foo&memo=467&start=500',
+ navigator.io_provider.last_request.url);
+ },
+ /**
+ * prev_batch uses values from current_batch.prev + direction=backwards
+ * and ordering=current_batch.ordering.
+ */
+ test_prev_batch: function(){
+ var navigator = get_navigator('?memo=pi&start=26');
+ navigator.prev_batch();
+ Y.Assert.areSame(
+ '/bar/+bugs/++model++?orderby=foo&memo=457&start=400&' +
+ 'direction=backwards',
+ navigator.io_provider.last_request.url);
+ }
+}));
+
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-17 19:38:35 +0000
+++ lib/lp/bugs/templates/buglisting-default.pt 2011-10-26 20:19:27 +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.update_listing('status');
+ var navigator = Y.lp.bugs.buglisting.ListingNavigator.from_page();
});
});
</script>
=== modified file 'lib/lp/bugs/templates/bugs-listing-table-without-navlinks.pt'
--- lib/lp/bugs/templates/bugs-listing-table-without-navlinks.pt 2011-10-13 21:47:54 +0000
+++ lib/lp/bugs/templates/bugs-listing-table-without-navlinks.pt 2011-10-26 20:19:27 +0000
@@ -76,12 +76,5 @@
</tr>
</tbody>
</table>
- <tal:mustache condition="request/features/bugs.dynamic_bug_listings.enabled">
- <em>Server-side mustache</em>
- <span tal:replace="structure context/mustache" />
- <em>Client-side mustache</em>
- <span id="client-listing" />
- <script tal:content="structure context/mustache_listings" />
- </tal:mustache>
</tal:results>
</tal:root>
=== modified file 'lib/lp/bugs/templates/bugs-listing-table.pt'
--- lib/lp/bugs/templates/bugs-listing-table.pt 2011-10-12 13:38:54 +0000
+++ lib/lp/bugs/templates/bugs-listing-table.pt 2011-10-26 20:19:27 +0000
@@ -15,9 +15,18 @@
</tal:no-results>
<tal:results condition="context/batch">
- <div class="lesser" tal:content="structure context/@@+navigation-links-upper" />
- <div tal:replace="structure context/@@+table-view-without-navlinks" />
- <div class="lesser" tal:content="structure context/@@+navigation-links-lower" />
+ <div class="lesser"
+ tal:content="structure context/@@+navigation-links-upper" />
+ <div tal:replace="structure context/@@+table-view-without-navlinks"
+ tal:condition="not: request/features/bugs.dynamic_bug_listings.enabled"
+ />
+ <tal:mustache
+ condition="request/features/bugs.dynamic_bug_listings.enabled">
+ <span id="client-listing" tal:content="structure context/mustache" />
+ <script tal:content="structure context/mustache_listings" />
+ </tal:mustache>
+ <div class="lesser"
+ tal:content="structure context/@@+navigation-links-lower" />
</tal:results>
<tal:comment
Follow ups