Merge lp:~abentley/launchpad/pre-cache-batches into lp:launchpad

Proposed by Aaron Bentley
Status: Merged
Merged at revision: 14352
Proposed branch: lp:~abentley/launchpad/pre-cache-batches
Merge into: lp:launchpad
Prerequisite: lp:~abentley/launchpad/view-flags
Diff against target: 429 lines (+224/-29)
4 files modified
lib/lp/bugs/browser/bugtask.py (+4/-1)
lib/lp/bugs/javascript/buglisting.js (+93/-25)
lib/lp/bugs/javascript/tests/test_buglisting.js (+121/-3)
lib/lp/services/features/flags.py (+6/-0)
To merge this branch: bzr merge lp:~abentley/launchpad/pre-cache-batches
Reviewer Review Type Date Requested Status
Brad Crittenden (community) code Approve
Review via email: mp+82716@code.launchpad.net

Commit message

Pre-fetch next batch of bug listings.

Description of the change

= Summary =
Implement limited pre-fetching of bug listings.

== Proposed fix ==
The "next" link is pre-fetched, but only if pre-fetching is activated.

== Pre-implementation notes ==
Discussed with deryck

== Implementation details ==
bugs.dynamic_bug_listings.pre_fetch must be enabled to see this in action.

bugs.dynamic_bug_listings.pre_fetch is declared to be related to the view, so that it is provided in the JSON cache.

The first_batch_config next_batch_config and prev_batch_config are extracted from first_batch, next_batch, and last_batch, because initially, all of those would be pre-fetched.

update ensures we do not pre-fetch batches we already have.

Several methods are updated to understand fetch_only, (i.e. don't render).

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

== Demo and Q/A ==
Ensure bugs.dynamic_bug_listings.pre_fetch is active for you. Go to a bugs listing page. Click next. It should update almost instantly, without a roundrip. (This will also cause it to pre-fetch the new next batch.)

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/app/javascript/beta-notification.js
  lib/lp/app/javascript/tests/test_beta_notification.js
  lib/canonical/launchpad/webapp/publisher.py
  lib/lp/bugs/javascript/buglisting.js
  lib/canonical/launchpad/webapp/tests/test_publisher.py
  lib/lp/app/javascript/tests/test_beta_notification.html
  lib/canonical/launchpad/webapp/tests/test_view_model.py
  lib/lp/services/features/flags.py
  lib/lp/bugs/browser/bugtask.py
  lib/lp/bugs/javascript/tests/test_buglisting.js

To post a comment you must log in.
Revision history for this message
Brad Crittenden (bac) wrote :

Hi Aaron,

Thanks for clearing up my confusion on IRC wrt the Y.bind issue.

typo: ajacent -> adjacent

Your tests provide good coverage and are readable. Thanks.

--Brad

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/bugs/browser/bugtask.py'
--- lib/lp/bugs/browser/bugtask.py 2011-11-18 20:16:26 +0000
+++ lib/lp/bugs/browser/bugtask.py 2011-11-18 20:16:27 +0000
@@ -2455,7 +2455,10 @@
24552455
2456 implements(IBugTaskSearchListingMenu)2456 implements(IBugTaskSearchListingMenu)
24572457
2458 related_features = ['bugs.dynamic_bug_listings.enabled']2458 related_features = (
2459 'bugs.dynamic_bug_listings.enabled',
2460 'bugs.dynamic_bug_listings.pre_fetch',
2461 )
24592462
2460 # Only include <link> tags for bug feeds when using this view.2463 # Only include <link> tags for bug feeds when using this view.
2461 feed_types = (2464 feed_types = (
24622465
=== modified file 'lib/lp/bugs/javascript/buglisting.js'
--- lib/lp/bugs/javascript/buglisting.js 2011-11-18 20:16:26 +0000
+++ lib/lp/bugs/javascript/buglisting.js 2011-11-18 20:16:27 +0000
@@ -40,6 +40,7 @@
40 backwards_navigation: {valueFn: empty_nodelist},40 backwards_navigation: {valueFn: empty_nodelist},
41 forwards_navigation: {valueFn: empty_nodelist},41 forwards_navigation: {valueFn: empty_nodelist},
42 io_provider: {value: null},42 io_provider: {value: null},
43 pre_fetch: {value: false},
43 navigation_indices: {valueFn: empty_nodelist}44 navigation_indices: {valueFn: empty_nodelist}
44};45};
4546
@@ -64,6 +65,7 @@
64 this.set('field_visibility', cache.field_visibility);65 this.set('field_visibility', cache.field_visibility);
65 batch_key = this.handle_new_batch(cache);66 batch_key = this.handle_new_batch(cache);
66 this.set('current_batch', cache);67 this.set('current_batch', cache);
68 this.pre_fetch_batches();
67 // Work around mustache.js bug 48 "Blank lines are not preserved."69 // Work around mustache.js bug 48 "Blank lines are not preserved."
68 // https://github.com/janl/mustache.js/issues/4870 // https://github.com/janl/mustache.js/issues/48
69 if (Y.Lang.isValue(template)) {71 if (Y.Lang.isValue(template)) {
@@ -90,6 +92,7 @@
90 var batch_key = e.newVal.batch_key;92 var batch_key = e.newVal.batch_key;
91 var batch = this.get('batches')[batch_key];93 var batch = this.get('batches')[batch_key];
92 this.set('current_batch', batch);94 this.set('current_batch', batch);
95 this.pre_fetch_batches();
93 this.render();96 this.render();
94 }97 }
95 },98 },
@@ -167,8 +170,12 @@
167 'inactive', !this.has_next());170 'inactive', !this.has_next());
168 },171 },
169172
170 update_from_new_model: function(query, model){173 update_from_new_model: function(query, fetch_only, model){
171 this.update_from_cache(query, this.handle_new_batch(model));174 var batch_key = this.handle_new_batch(model);
175 if (fetch_only) {
176 return;
177 }
178 this.update_from_cache(query, batch_key);
172 },179 },
173180
174 /**181 /**
@@ -202,6 +209,22 @@
202 return query;209 return query;
203 },210 },
204211
212
213 /**
214 * Pre-fetch adjacent batches.
215 */
216 pre_fetch_batches: function(){
217 var that=this;
218 if (!this.get('pre_fetch')){
219 return;
220 }
221 Y.each(this.get_pre_fetch_configs(), function(config){
222 config.fetch_only = true;
223 that.update(config);
224 });
225 },
226
227
205 /**228 /**
206 * Update the display to the specified batch.229 * Update the display to the specified batch.
207 *230 *
@@ -213,10 +236,13 @@
213 var cached_batch = this.get('batches')[key];236 var cached_batch = this.get('batches')[key];
214 var query = this.get_batch_query(config);237 var query = this.get_batch_query(config);
215 if (Y.Lang.isValue(cached_batch)) {238 if (Y.Lang.isValue(cached_batch)) {
239 if (config.fetch_only){
240 return;
241 }
216 this.update_from_cache(query, key);242 this.update_from_cache(query, key);
217 }243 }
218 else {244 else {
219 this.load_model(query);245 this.load_model(query, config.fetch_only);
220 }246 }
221 },247 },
222248
@@ -232,53 +258,70 @@
232 });258 });
233 },259 },
234260
235 /**261 first_batch_config: function(order_by){
236 * Update the navigator to display the first batch.
237 *
238 * The order_by defaults to the current ordering, but may be overridden.
239 */
240 first_batch: function(order_by) {
241 if (order_by === undefined) {262 if (order_by === undefined) {
242 order_by = this.get('current_batch').order_by;263 order_by = this.get('current_batch').order_by;
243 }264 }
244 this.update({265 return {
245 forwards: true,266 forwards: true,
246 memo: null,267 memo: null,
247 start: 0,268 start: 0,
248 order_by: order_by269 order_by: order_by
249 });270 };
250 },271 },
251272
252 /**273 /**
253 * Update the navigator to display the next batch.274 * Update the navigator to display the first batch.
275 *
276 * The order_by defaults to the current ordering, but may be overridden.
254 */277 */
255 next_batch: function() {278 first_batch: function(order_by) {
279 this.update(this.first_batch_config(order_by));
280 },
281
282 next_batch_config: function(){
256 var current_batch = this.get('current_batch');283 var current_batch = this.get('current_batch');
257 if (!this.has_next()){284 if (!this.has_next()){
258 return;285 return null;
259 }286 }
260 this.update({287 return {
261 forwards: true,288 forwards: true,
262 memo: current_batch.next.memo,289 memo: current_batch.next.memo,
263 start: current_batch.next.start,290 start: current_batch.next.start,
264 order_by: current_batch.order_by291 order_by: current_batch.order_by
265 });292 };
266 },293 },
267
268 /**294 /**
269 * Update the navigator to display the previous batch.295 * Update the navigator to display the next batch.
270 */296 */
271 prev_batch: function() {297 next_batch: function() {
298 var config = this.next_batch_config();
299 if (config === null){
300 return;
301 }
302 this.update(config);
303 },
304 prev_batch_config: function(){
272 var current_batch = this.get('current_batch');305 var current_batch = this.get('current_batch');
273 if (!this.has_prev()){306 if (!this.has_prev()){
274 return;307 return null;
275 }308 }
276 this.update({309 return {
277 forwards: false,310 forwards: false,
278 memo: current_batch.prev.memo,311 memo: current_batch.prev.memo,
279 start: current_batch.prev.start,312 start: current_batch.prev.start,
280 order_by: current_batch.order_by313 order_by: current_batch.order_by
281 });314 };
315 },
316 /**
317 * Update the navigator to display the previous batch.
318 */
319 prev_batch: function() {
320 var config = this.prev_batch_config();
321 if (config === null){
322 return;
323 }
324 this.update(config);
282 },325 },
283 /**326 /**
284 * Change which fields are displayed in the batch. Input is a config with327 * Change which fields are displayed in the batch. Input is a config with
@@ -291,15 +334,28 @@
291 },334 },
292335
293 /**336 /**
337 * Generate a list of configs to pre-fetch.
338 */
339 get_pre_fetch_configs: function(){
340 var configs = [];
341 var next_batch_config = this.next_batch_config();
342 if (next_batch_config !== null){
343 configs.push(next_batch_config);
344 }
345 return configs;
346 },
347
348 /**
294 * Load the specified batch via ajax. Display & cache on load.349 * Load the specified batch via ajax. Display & cache on load.
295 *350 *
296 * query is the query string for the URL, as a mapping. (See351 * query is the query string for the URL, as a mapping. (See
297 * get_batch_query).352 * get_batch_query).
298 */353 */
299 load_model: function(query) {354 load_model: function(query, fetch_only) {
300 var load_model_config = {355 var load_model_config = {
301 on: {356 on: {
302 success: Y.bind(this.update_from_new_model, this, query),357 success: Y.bind(
358 this.update_from_new_model, this, query, fetch_only),
303 failure: this.get('failure_handler')359 failure: this.get('failure_handler')
304 }360 }
305 };361 };
@@ -328,18 +384,30 @@
328 });384 });
329};385};
330386
387
388/**
389 * Return the value for a given feature flag in the current scope.
390 * Only flags declared as "related_features" on the view are available.
391 */
392var get_feature_flag = function(flag_name){
393 return LP.cache.related_features[flag_name].value;
394};
395
396
331/**397/**
332 * Factory to return a ListingNavigator for the given page.398 * Factory to return a ListingNavigator for the given page.
333 */399 */
334namespace.ListingNavigator.from_page = function() {400namespace.ListingNavigator.from_page = function() {
335 var target = Y.one('#client-listing');401 var target = Y.one('#client-listing');
336 var navigation_indices = Y.all('.batch-navigation-index');402 var navigation_indices = Y.all('.batch-navigation-index');
403 var pre_fetch = get_feature_flag('bugs.dynamic_bug_listings.pre_fetch');
337 var navigator = new namespace.ListingNavigator({404 var navigator = new namespace.ListingNavigator({
338 current_url: window.location,405 current_url: window.location,
339 cache: LP.cache,406 cache: LP.cache,
340 template: LP.mustache_listings,407 template: LP.mustache_listings,
341 target: target,408 target: target,
342 navigation_indices: navigation_indices409 navigation_indices: navigation_indices,
410 pre_fetch: Boolean(pre_fetch)
343 });411 });
344 namespace.linkify_navigation();412 namespace.linkify_navigation();
345 navigator.set('backwards_navigation', Y.all('.first,.previous'));413 navigator.set('backwards_navigation', Y.all('.first,.previous'));
346414
=== modified file 'lib/lp/bugs/javascript/tests/test_buglisting.js'
--- lib/lp/bugs/javascript/tests/test_buglisting.js 2011-11-18 20:16:26 +0000
+++ lib/lp/bugs/javascript/tests/test_buglisting.js 2011-11-18 20:16:27 +0000
@@ -59,7 +59,7 @@
59 prev: null59 prev: null
60 };60 };
61 var query = navigator.get_batch_query(batch);61 var query = navigator.get_batch_query(batch);
62 navigator.update_from_new_model(query, batch);62 navigator.update_from_new_model(query, false, batch);
63 bugtask = navigator.get('current_batch').mustache_model.bugtasks[0];63 bugtask = navigator.get('current_batch').mustache_model.bugtasks[0];
64 Y.Assert.isFalse(bugtask.hasOwnProperty('show_item'));64 Y.Assert.isFalse(bugtask.hasOwnProperty('show_item'));
65 }65 }
@@ -330,7 +330,7 @@
330 bugtasks: ['a', 'b', 'c']330 bugtasks: ['a', 'b', 'c']
331 }};331 }};
332 var query = navigator.get_batch_query(batch);332 var query = navigator.get_batch_query(batch);
333 navigator.update_from_new_model(query, batch);333 navigator.update_from_new_model(query, true, batch);
334 Y.lp.testing.assert.assert_equal_structure(334 Y.lp.testing.assert.assert_equal_structure(
335 batch, navigator.get('batches')[key]);335 batch, navigator.get('batches')[key]);
336 },336 },
@@ -470,6 +470,7 @@
470 current_url: url,470 current_url: url,
471 cache: lp_cache,471 cache: lp_cache,
472 io_provider: mock_io,472 io_provider: mock_io,
473 pre_fetch: config.pre_fetch,
473 target: target,474 target: target,
474 template: ''475 template: ''
475 });476 });
@@ -605,7 +606,10 @@
605 cache: {606 cache: {
606 current_batch: {},607 current_batch: {},
607 next: null,608 next: null,
608 prev: null609 prev: null,
610 related_features: {
611 'bugs.dynamic_bug_listings.pre_fetch': {value: 'on'}
612 }
609 }613 }
610 };614 };
611 },615 },
@@ -626,6 +630,120 @@
626 }630 }
627}));631}));
628632
633suite.add(new Y.Test.Case({
634 name: "pre-fetching batches",
635
636 /**
637 * get_pre_fetch_configs should return a config for the next batch.
638 */
639 test_get_pre_fetch_configs: function(){
640 var navigator = get_navigator();
641 var configs = navigator.get_pre_fetch_configs();
642 var batch_keys = [];
643 Y.each(configs, function(value){
644 batch_keys.push(navigator.constructor.get_batch_key(value));
645 });
646 Y.Assert.areSame('["foo",467,true,500]', batch_keys[0]);
647 Y.Assert.areSame(1, batch_keys.length);
648 },
649
650 /**
651 * get_pre_fetch_configs should return an empty list if no next batch.
652 */
653 test_get_pre_fetch_configs_no_next: function(){
654 var navigator = get_navigator('', {no_next: true});
655 var configs = navigator.get_pre_fetch_configs();
656 var batch_keys = [];
657 Y.each(configs, function(value){
658 batch_keys.push(navigator.constructor.get_batch_key(value));
659 });
660 Y.Assert.areSame(0, batch_keys.length);
661 },
662
663 get_pre_fetch_navigator: function(config){
664 var navigator = get_navigator('', config);
665 var lp_client = new Y.lp.client.Launchpad();
666 var batch = lp_client.wrap_resource(null, {
667 context: {
668 resource_type_link: 'http://foo_type',
669 web_link: 'http://foo/bar'
670 },
671 next: {memo: 57, start: 56}});
672 navigator.set('current_batch', batch);
673 return navigator;
674 },
675
676 /**
677 * Calling pre_fetch_batches should produce a request for the next batch.
678 */
679 test_pre_fetch_batches: function(){
680 var navigator = this.get_pre_fetch_navigator();
681 var io_provider = navigator.get('io_provider');
682 navigator.set('pre_fetch', true);
683 Y.Assert.isNull(io_provider.last_request);
684 navigator.pre_fetch_batches();
685 Y.Assert.areSame(
686 io_provider.last_request.url,
687 '/bar/+bugs/++model++?foo=bar&orderby=&memo=57&start=56');
688 },
689
690 /**
691 * Calling pre_fetch_batches should not produce a request for the next
692 * batch if Navigator.get('pre_fetch') is false.
693 */
694 test_pre_fetch_disabled: function(){
695 var last_url;
696 var navigator = this.get_pre_fetch_navigator();
697 navigator.pre_fetch_batches();
698 Y.Assert.areSame(null, navigator.get('io_provider').last_request);
699 },
700
701 /**
702 * Initialization does a pre-fetch.
703 */
704 test_pre_fetch_on_init: function(){
705 var navigator = get_navigator('', {pre_fetch: true});
706 var last_url = navigator.get('io_provider').last_request.url;
707 Y.Assert.areSame(
708 last_url,
709 '/bar/+bugs/++model++?foo=bar&orderby=foo&memo=467&start=500');
710 },
711 /**
712 * update_from_new_model does a pre-fetch.
713 */
714 test_pre_fetch_on_update_from_new_model: function(){
715 var navigator = get_navigator('');
716 var io_provider = navigator.get('io_provider');
717 var lp_client = new Y.lp.client.Launchpad();
718 var batch = lp_client.wrap_resource(null, {
719 context: {
720 resource_type_link: 'http://foo_type',
721 web_link: 'http://foo/bar'
722 },
723 order_by: 'baz',
724 memo: 'memo1',
725 next: {
726 memo: "pi",
727 start: 314
728 },
729 forwards: true,
730 start: 5,
731 mustache_model: {
732 item: [
733 {name: 'first'},
734 {name: 'second'}
735 ],
736 bugtasks: ['a', 'b', 'c']
737 }});
738 Y.Assert.isNull(io_provider.last_request);
739 navigator.set('pre_fetch', true);
740 navigator.update_from_new_model({}, false, batch);
741 Y.Assert.areSame(
742 io_provider.last_request.url,
743 '/bar/+bugs/++model++?foo=bar&orderby=baz&memo=pi&start=314');
744 }
745}));
746
629var handle_complete = function(data) {747var handle_complete = function(data) {
630 window.status = '::::' + JSON.stringify(data);748 window.status = '::::' + JSON.stringify(data);
631 };749 };
632750
=== modified file 'lib/lp/services/features/flags.py'
--- lib/lp/services/features/flags.py 2011-11-16 23:46:01 +0000
+++ lib/lp/services/features/flags.py 2011-11-18 20:16:27 +0000
@@ -65,6 +65,12 @@
65 '',65 '',
66 'Dynamic bug listings',66 'Dynamic bug listings',
67 'https://dev.launchpad.net/LEP/CustomBugListings'),67 'https://dev.launchpad.net/LEP/CustomBugListings'),
68 ('bugs.dynamic_bug_listings.pre_fetch',
69 'boolean',
70 ('Enables pre-fetching bug listing results.'),
71 '',
72 'Listing pre-fetching',
73 'https://dev.launchpad.net/LEP/CustomBugListings'),
68 ('code.ajax_revision_diffs.enabled',74 ('code.ajax_revision_diffs.enabled',
69 'boolean',75 'boolean',
70 ("Offer expandable inline diffs for branch revisions."),76 ("Offer expandable inline diffs for branch revisions."),