launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #05368
[Merge] lp:~julian-edwards/launchpad/cancel-build-bug-173018-ui-part4 into lp:launchpad
Julian Edwards has proposed merging lp:~julian-edwards/launchpad/cancel-build-bug-173018-ui-part4 into lp:launchpad with lp:~julian-edwards/launchpad/cancel-build-bug-173018-ui-part3 as a prerequisite.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
Related bugs:
Bug #173018 in Launchpad itself: "In progress builds cannot be cancelled"
https://bugs.launchpad.net/launchpad/+bug/173018
For more details, see:
https://code.launchpad.net/~julian-edwards/launchpad/cancel-build-bug-173018-ui-part4/+merge/80997
= Summary =
Final branch in pipeline of changes to add build cancellation.
== Proposed fix ==
Add a cancellation link/button on build pages.
== Pre-implementation notes ==
Pre-what? :)
== Implementation details ==
Similar to the cancellation for recipe builds, this adds a traversal to
+cancel from a binary build which uses the general edit template.
Most of the change is pretty boilerplate.
== Tests ==
bin/test -cvv test_build_views
== Demo and Q/A ==
Already tested on Dogfood, works like a charm.
--
https://code.launchpad.net/~julian-edwards/launchpad/cancel-build-bug-173018-ui-part4/+merge/80997
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~julian-edwards/launchpad/cancel-build-bug-173018-ui-part4 into lp:launchpad.
=== modified file 'lib/lp/bugs/browser/bugtask.py'
--- lib/lp/bugs/browser/bugtask.py 2011-11-01 15:43:30 +0000
+++ lib/lp/bugs/browser/bugtask.py 2011-11-02 10:50:34 +0000
@@ -2484,25 +2484,48 @@
self.context, self.request, self.user)
if getFeatureFlag('bugs.dynamic_bug_listings.enabled'):
cache = IJSONRequestCache(self.request)
- 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))
+<<<<<<< TREE
+ 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))
+=======
+ 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['total'] = batch_navigator.batch.total()
+ 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))
+>>>>>>> MERGE-SOURCE
@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-11-01 15:43:30 +0000
+++ lib/lp/bugs/browser/tests/test_bugtask.py 2011-11-02 10:50:34 +0000
@@ -1351,90 +1351,178 @@
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'])
-
+<<<<<<< TREE
+ 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 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'])
+ self.assertEqual(1, cache.objects['total'])
+
+ 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'])
+
+>>>>>>> MERGE-SOURCE
def getBugtaskBrowser(self):
"""Return a browser for a new bugtask."""
bugtask = self.factory.makeBugTask()
=== modified file 'lib/lp/bugs/javascript/buglisting.js'
--- lib/lp/bugs/javascript/buglisting.js 2011-11-01 15:43:30 +0000
+++ lib/lp/bugs/javascript/buglisting.js 2011-11-02 10:50:34 +0000
@@ -13,6 +13,7 @@
/**
+<<<<<<< TREE
* Constructor.
* current_url is used to determine search params.
* cache is the JSONRequestCache for the batch.
@@ -70,6 +71,95 @@
/**
+=======
+ * 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.
+ * navigation_indices is a YUI NodeList of nodes to update with the current
+ * batch info.
+ * 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,
+ navigation_indices, 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 = {};
+ this.backwards_navigation = new Y.NodeList([]);
+ this.forwards_navigation = new Y.NodeList([]);
+ if (!Y.Lang.isValue(navigation_indices)){
+ navigation_indices = new Y.NodeList([]);
+ }
+ this.navigation_indices = navigation_indices;
+ this.batch_info_template = '<strong>{{start}}</strong> → ' +
+ '<strong>{{end}}</strong> of {{total}} results';
+};
+
+
+/**
+ * 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');
+};
+
+/**
+ * Rewrite all nodes with navigation classes so that they are hyperlinks.
+ * Content is retained.
+ */
+namespace.linkify_navigation = function(){
+ Y.each(['previous', 'next', 'first', 'last'], function(class_name){
+ Y.all('.' + class_name).each(function(node){
+ new_node = Y.Node.create('<a href="#"></a>');
+ new_node.addClass(class_name);
+ new_node.setContent(node.getContent());
+ node.replace(new_node);
+ });
+ });
+};
+
+/**
+ * Factory to return a ListingNavigator for the given page.
+ */
+namespace.ListingNavigator.from_page = function(){
+ var target = Y.one('#client-listing');
+ var navigation_indices = Y.all('.batch-navigation-index');
+ var navigator = new namespace.ListingNavigator(
+ window.location, LP.cache, LP.mustache_listings, target,
+ navigation_indices);
+ namespace.linkify_navigation();
+ navigator.backwards_navigation = Y.all('.first,.previous');
+ navigator.forwards_navigation = Y.all('.last,.next');
+ navigator.clickAction('.first', navigator.first_batch);
+ navigator.clickAction('.next', navigator.next_batch);
+ navigator.clickAction('.previous', navigator.prev_batch);
+ navigator.clickAction('.last', navigator.last_batch);
+ navigator.render_navigation();
+ return navigator;
+};
+
+
+/**
+>>>>>>> MERGE-SOURCE
* Render bug listings via Mustache.
*
* If model is supplied, it is used as the data for rendering the listings.
@@ -77,10 +167,16 @@
*
* The template is always LP.mustache_listings.
*/
+<<<<<<< TREE
namespace.ListingNavigator.prototype.rendertable = function(){
if (! Y.Lang.isValue(this.target)){
+=======
+namespace.ListingNavigator.prototype.render = function(){
+ if (! Y.Lang.isValue(this.target)){
+>>>>>>> MERGE-SOURCE
return;
}
+<<<<<<< TREE
var model = this.current_batch.mustache_model;
var txt = Mustache.to_html(this.template, model);
this.target.set('innerHTML', txt);
@@ -93,6 +189,38 @@
namespace.ListingNavigator.get_batch_key = function(config){
return JSON.stringify([config.order_by, config.memo, config.forwards,
config.start]);
+=======
+ var model = this.current_batch.mustache_model;
+ var batch_info = Mustache.to_html(this.batch_info_template, {
+ start: this.current_batch.start + 1,
+ end: this.current_batch.start +
+ this.current_batch.mustache_model.bugtasks.length,
+ total: this.current_batch.total
+ });
+ this.target.setContent(Mustache.to_html(this.template, model));
+ this.navigation_indices.setContent(batch_info);
+ this.render_navigation();
+};
+
+
+/**
+ * Enable/disable navigation links as appropriate.
+ */
+namespace.ListingNavigator.prototype.render_navigation = function(){
+ this.backwards_navigation.toggleClass(
+ 'inactive', this.current_batch.prev === null);
+ this.forwards_navigation.toggleClass(
+ 'inactive', this.current_batch.next === null);
+};
+
+
+/**
+ * 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]);
+>>>>>>> MERGE-SOURCE
};
/**
@@ -101,6 +229,7 @@
*
* order_by is the ordering used by the model.
*/
+<<<<<<< TREE
namespace.ListingNavigator.prototype.update_from_model = function(model){
var key = namespace.ListingNavigator.get_batch_key(model);
this.batches[key] = model;
@@ -209,6 +338,116 @@
namespace.ListingNavigator.prototype.load_model = function(config){
var query = this.get_batch_query(config);
var 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.render();
+};
+
+
+/**
+ * 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.render();
+ }
+ 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 = {
+>>>>>>> MERGE-SOURCE
on: {
success: Y.bind(this.update_from_model, this)
}
=== modified file 'lib/lp/bugs/javascript/tests/test_buglisting.js'
--- lib/lp/bugs/javascript/tests/test_buglisting.js 2011-11-01 15:43:30 +0000
+++ lib/lp/bugs/javascript/tests/test_buglisting.js 2011-11-02 10:50:34 +0000
@@ -8,6 +8,7 @@
var suite = new Y.Test.Suite("lp.bugs.buglisting Tests");
var module = Y.lp.bugs.buglisting;
+<<<<<<< TREE
suite.add(new Y.Test.Case({
name: 'ListingNavigator',
@@ -24,12 +25,65 @@
name: 'rendertable',
test_rendertable_no_client_listing: function() {
+=======
+
+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: 'render',
+
+ tearDown: function(){
+ Y.one('#fixture').setContent('');
+ },
+
+ test_render_no_client_listing: function() {
+>>>>>>> MERGE-SOURCE
// Rendering should not error with no #client-listing.
+<<<<<<< TREE
var navigator = new module.ListingNavigator();
navigator.rendertable();
},
test_rendertable: function() {
+=======
+ var navigator = new module.ListingNavigator();
+ navigator.render();
+ },
+ get_render_navigator: function() {
+ var target = Y.Node.create('<div id="client-listing"></div>');
+ var lp_cache = {
+ mustache_model: {
+ foo: 'bar',
+ bugtasks: ['a', 'b', 'c']
+ },
+ next: null,
+ prev: null,
+ start: 5,
+ total: 256
+ };
+ var template = "{{foo}}";
+ var navigator = new module.ListingNavigator(
+ null, lp_cache, template, target);
+ var index = Y.Node.create(
+ '<div><strong>3</strong> → <strong>4</strong>' +
+ ' of 512 results</div>');
+ navigator.navigation_indices.push(index);
+ navigator.backwards_navigation.push(Y.Node.create('<div></div>'));
+ navigator.forwards_navigation.push(Y.Node.create('<div></div>'));
+ return navigator;
+ },
+ test_render: function() {
+>>>>>>> MERGE-SOURCE
// Rendering should work with #client-listing supplied.
+<<<<<<< TREE
var target = Y.Node.create('<div id="client-listing"></div>');
var lp_cache = {
mustache_model: {
@@ -41,6 +95,98 @@
null, lp_cache, template, target);
navigator.rendertable();
Y.Assert.areEqual('bar', navigator.target.get('innerHTML'));
+=======
+ var navigator = this.get_render_navigator();
+ navigator.render();
+ Y.Assert.areEqual('bar', navigator.target.getContent());
+ },
+ /**
+ * render_navigation should disable "previous" and "first" if there is
+ * no previous batch (i.e. we're at the beginning.)
+ */
+ test_render_navigation_disables_backwards_navigation_if_no_prev:
+ function() {
+ var navigator = this.get_render_navigator();
+ var action = navigator.backwards_navigation.item(0);
+ navigator.render_navigation();
+ Y.Assert.isTrue(action.hasClass('inactive'));
+ },
+ /**
+ * render_navigation should enable "previous" and "first" if there is
+ * a previous batch (i.e. we're not at the beginning.)
+ */
+ test_render_navigation_enables_backwards_navigation_if_prev:
+ function() {
+ var navigator = this.get_render_navigator();
+ var action = navigator.backwards_navigation.item(0);
+ action.addClass('inactive');
+ navigator.current_batch.prev = {
+ start: 1, memo: 'pi'
+ };
+ navigator.render_navigation();
+ Y.Assert.isFalse(action.hasClass('inactive'));
+ },
+ /**
+ * render_navigation should disable "next" and "last" if there is
+ * no next batch (i.e. we're at the end.)
+ */
+ test_render_navigation_disables_forwards_navigation_if_no_next:
+ function() {
+ var navigator = this.get_render_navigator();
+ var action = navigator.forwards_navigation.item(0);
+ navigator.render_navigation();
+ Y.Assert.isTrue(action.hasClass('inactive'));
+ },
+ /**
+ * render_navigation should enable "next" and "last" if there is a next
+ * batch (i.e. we're not at the end.)
+ */
+ test_render_navigation_enables_forwards_navigation_if_next: function() {
+ var navigator = this.get_render_navigator();
+ var action = navigator.forwards_navigation.item(0);
+ action.addClass('inactive');
+ navigator.current_batch.next = {
+ start: 1, memo: 'pi'
+ };
+ navigator.render_navigation();
+ Y.Assert.isFalse(action.hasClass('inactive'));
+ },
+ /**
+ * linkify_navigation should convert previous, next, first last into
+ * hyperlinks, while retaining the original content.
+ */
+ test_linkify_navigation: function(){
+ Y.one('#fixture').setContent(
+ '<span class="previous">PreVious</span>' +
+ '<span class="next">NeXt</span>' +
+ '<span class="first">FiRST</span>' +
+ '<span class="last">lAst</span>');
+ module.linkify_navigation();
+ function checkNav(selector, content){
+ var node = Y.one(selector);
+ Y.Assert.areEqual('a', node.get('tagName').toLowerCase());
+ Y.Assert.areEqual(content, node.getContent());
+ Y.Assert.areEqual('#', node.get('href').substr(-1, 1));
+ }
+ checkNav('.previous', 'PreVious');
+ checkNav('.next', 'NeXt');
+ checkNav('.first', 'FiRST');
+ checkNav('.last', 'lAst');
+ },
+ /**
+ * Render should update the navigation_indices with the result info.
+ */
+ test_render_navigation_indices: function(){
+ var navigator = this.get_render_navigator();
+ index = navigator.navigation_indices.item(0);
+ Y.Assert.areEqual(
+ '<strong>3</strong> \u2192 <strong>4</strong> of 512 results',
+ index.getContent());
+ navigator.render();
+ Y.Assert.areEqual(
+ '<strong>6</strong> \u2192 <strong>8</strong> of 256 results',
+ index.getContent());
+>>>>>>> MERGE-SOURCE
}
}));
@@ -52,6 +198,7 @@
*/
get_intensity_listing: function(){
mock_io = new Y.lp.testing.mockio.MockIo();
+<<<<<<< TREE
lp_cache = {
context: {
resource_type_link: 'http://foo_type',
@@ -75,13 +222,50 @@
},
mustache_model:
{item: [
+=======
+ lp_cache = {
+ context: {
+ resource_type_link: 'http://foo_type',
+ web_link: 'http://foo/bar'
+ },
+ mustache_model: {
+ foo: 'bar',
+ bugtasks: []
+ }
+ };
+ 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, null, mock_io);
+ navigator.first_batch('intensity');
+ Y.Assert.areEqual('', navigator.target.getContent());
+ mock_io.last_request.successJSON({
+ context: {
+ resource_type_link: 'http://foo_type',
+ web_link: 'http://foo/bar'
+ },
+ mustache_model:
+ {
+ item: [
+>>>>>>> MERGE-SOURCE
{name: 'first'},
+<<<<<<< TREE
{name: 'second'}
]},
order_by: 'intensity',
start: 0,
forwards: true,
memo: null
+=======
+ {name: 'second'}],
+ bugtasks: []
+ },
+ order_by: 'intensity',
+ start: 0,
+ forwards: true,
+ memo: null
+>>>>>>> MERGE-SOURCE
});
return navigator;
},
@@ -91,8 +275,13 @@
var navigator = this.get_intensity_listing();
var mock_io = navigator.io_provider;
Y.Assert.areEqual('<ol><li>first</li><li>second</li></ol>',
+<<<<<<< TREE
navigator.target.get('innerHTML'));
Y.Assert.areEqual('/bar/+bugs/++model++?orderby=intensity&start=0',
+=======
+ navigator.target.getContent());
+ Y.Assert.areEqual('/bar/+bugs/++model++?orderby=intensity&start=0',
+>>>>>>> MERGE-SOURCE
mock_io.last_request.url);
},
test_first_batch_uses_cache: function() {
@@ -166,6 +355,7 @@
}
}));
+<<<<<<< TREE
suite.add(new Y.Test.Case({
name: 'get_batch_url',
@@ -301,6 +491,144 @@
}
}));
+=======
+
+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, 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);
+ }
+}));
+
+>>>>>>> MERGE-SOURCE
var handle_complete = function(data) {
window.status = '::::' + JSON.stringify(data);
};
=== modified file 'lib/lp/soyuz/browser/build.py'
--- lib/lp/soyuz/browser/build.py 2011-09-20 14:29:43 +0000
+++ lib/lp/soyuz/browser/build.py 2011-11-02 10:50:34 +0000
@@ -7,6 +7,7 @@
__all__ = [
'BuildBreadcrumb',
+ 'BuildCancelView',
'BuildContextMenu',
'BuildNavigation',
'BuildNavigationMixin',
@@ -21,7 +22,10 @@
from lazr.delegates import delegates
from lazr.restful.utils import safe_hasattr
from zope.component import getUtility
-from zope.interface import implements
+from zope.interface import (
+ implements,
+ Interface,
+ )
from canonical.launchpad import _
from canonical.launchpad.browser.librarian import (
@@ -156,7 +160,7 @@
"""Overview menu for build records """
usedfor = IBinaryPackageBuild
- links = ['ppa', 'records', 'retry', 'rescore']
+ links = ['ppa', 'records', 'retry', 'rescore', 'cancel']
@property
def is_ppa_build(self):
@@ -189,6 +193,14 @@
'+rescore', text, icon='edit',
enabled=self.context.can_be_rescored)
+ @enabled_with_permission('launchpad.Edit')
+ def cancel(self):
+ """Only enabled for pending/active virtual builds."""
+ text = 'Cancel build'
+ return Link(
+ '+cancel', text, icon='edit',
+ enabled=self.context.can_be_cancelled)
+
class BuildBreadcrumb(Breadcrumb):
"""Builds a breadcrumb for an `IBinaryPackageBuild`."""
@@ -214,6 +226,8 @@
def label(self):
return self.context.title
+ page_title = label
+
@property
def user_can_retry_build(self):
"""Return True if the user is permitted to Retry Build.
@@ -397,6 +411,32 @@
"Build rescored to %s." % score)
+class BuildCancelView(LaunchpadFormView):
+ """View class for build cancellation."""
+
+ class schema(Interface):
+ """Schema for cancelling a build."""
+
+ page_title = label = "Cancel build"
+
+ @property
+ def cancel_url(self):
+ return canonical_url(self.context)
+ next_url = cancel_url
+
+ @action("Cancel build", name="cancel")
+ def request_action(self, action, data):
+ """Cancel the build."""
+ self.context.cancel()
+ if self.context.status == BuildStatus.CANCELLING:
+ self.request.response.addNotification(
+ "Build cancellation in progress")
+ elif self.context.status == BuildStatus.CANCELLED:
+ self.request.response.addNotification("Build cancelled")
+ else:
+ self.request.response.addNotification("Unable to cancel build")
+
+
class CompleteBuild:
"""Super object to store related IBinaryPackageBuild & IBuildQueue."""
delegates(IBinaryPackageBuild)
=== modified file 'lib/lp/soyuz/browser/configure.zcml'
--- lib/lp/soyuz/browser/configure.zcml 2011-10-23 02:04:25 +0000
+++ lib/lp/soyuz/browser/configure.zcml 2011-11-02 10:50:34 +0000
@@ -401,6 +401,15 @@
facet="overview"
template="../templates/build-rescore.pt"/>
</browser:pages>
+ <browser:pages
+ for="lp.soyuz.interfaces.binarypackagebuild.IBinaryPackageBuild"
+ class="lp.soyuz.browser.build.BuildCancelView"
+ permission="launchpad.Edit">
+ <browser:page
+ name="+cancel"
+ facet="overview"
+ template="../../app/templates/generic-edit.pt"/>
+ </browser:pages>
<browser:menus
classes="
BuildContextMenu"
=== modified file 'lib/lp/soyuz/browser/tests/test_build_views.py'
--- lib/lp/soyuz/browser/tests/test_build_views.py 2011-09-20 14:18:48 +0000
+++ lib/lp/soyuz/browser/tests/test_build_views.py 2011-11-02 10:50:34 +0000
@@ -8,6 +8,7 @@
timedelta,
)
import pytz
+import soupmatchers
from testtools.matchers import (
MatchesException,
Not,
@@ -60,12 +61,15 @@
# archive. For instance the 'PPA' action-menu link is not enabled
# for builds targeted to the PRIMARY archive.
archive = self.factory.makeArchive(purpose=ArchivePurpose.PRIMARY)
+ removeSecurityProxy(archive).require_virtualized = False
build = self.factory.makeBinaryPackageBuild(archive=archive)
build_menu = BuildContextMenu(build)
self.assertEquals(build_menu.links,
- ['ppa', 'records', 'retry', 'rescore'])
+ ['ppa', 'records', 'retry', 'rescore', 'cancel'])
self.assertFalse(build_menu.is_ppa_build)
self.assertFalse(build_menu.ppa().enabled)
+ # Cancel is not enabled on non-virtual builds.
+ self.assertFalse(build_menu.cancel().enabled)
def test_build_menu_ppa(self):
# The 'PPA' action-menu item will be enabled if we target the build
@@ -74,9 +78,11 @@
build = self.factory.makeBinaryPackageBuild(archive=ppa)
build_menu = BuildContextMenu(build)
self.assertEquals(build_menu.links,
- ['ppa', 'records', 'retry', 'rescore'])
+ ['ppa', 'records', 'retry', 'rescore', 'cancel'])
self.assertTrue(build_menu.is_ppa_build)
self.assertTrue(build_menu.ppa().enabled)
+ # Cancel is enabled on virtual builds.
+ self.assertTrue(build_menu.cancel().enabled)
def test_cannot_retry_stable_distroseries(self):
# 'BuildView.user_can_retry_build' property checks not only the
@@ -236,6 +242,61 @@
self.assertEquals(notification.message, "Build rescored to 0.")
self.assertEquals(pending_build.buildqueue_record.lastscore, 0)
+ def test_build_page_has_cancel_link(self):
+ build = self.factory.makeBinaryPackageBuild()
+ build.queueBuild()
+ person = build.archive.owner
+ with person_logged_in(person):
+ build_view = create_initialized_view(
+ build, "+index", principal=person)
+ page = build_view()
+ url = canonical_url(build) + "/+cancel"
+ matches_cancel_link = soupmatchers.HTMLContains(
+ soupmatchers.Tag(
+ "CANCEL_LINK", "a", attrs=dict(href=url)))
+ self.assertThat(page, matches_cancel_link)
+
+ def test_cancelling_pending_build(self):
+ ppa = self.factory.makeArchive(purpose=ArchivePurpose.PPA)
+ pending_build = self.factory.makeBinaryPackageBuild(archive=ppa)
+ pending_build.queueBuild()
+ with person_logged_in(ppa.owner):
+ view = create_initialized_view(
+ pending_build, name="+cancel", form={
+ 'field.actions.cancel': 'Cancel'})
+ notification = view.request.response.notifications[0]
+ self.assertEqual(notification.message, "Build cancelled")
+ self.assertEqual(BuildStatus.CANCELLED, pending_build.status)
+
+ def test_cancelling_building_build(self):
+ ppa = self.factory.makeArchive(purpose=ArchivePurpose.PPA)
+ pending_build = self.factory.makeBinaryPackageBuild(archive=ppa)
+ pending_build.queueBuild()
+ removeSecurityProxy(pending_build).status = BuildStatus.BUILDING
+ with person_logged_in(ppa.owner):
+ view = create_initialized_view(
+ pending_build, name="+cancel", form={
+ 'field.actions.cancel': 'Cancel'})
+ notification = view.request.response.notifications[0]
+ self.assertEqual(
+ notification.message, "Build cancellation in progress")
+ self.assertEqual(BuildStatus.CANCELLING, pending_build.status)
+
+ def test_cancelling_uncancellable_build(self):
+ archive = self.factory.makeArchive(purpose=ArchivePurpose.PRIMARY)
+ removeSecurityProxy(archive).require_virtualized = False
+ pending_build = self.factory.makeBinaryPackageBuild(archive=archive)
+ pending_build.queueBuild()
+ removeSecurityProxy(pending_build).status = BuildStatus.BUILDING
+ with person_logged_in(archive.owner):
+ view = create_initialized_view(
+ pending_build, name="+cancel", form={
+ 'field.actions.cancel': 'Cancel'})
+ notification = view.request.response.notifications[0]
+ self.assertEqual(
+ notification.message, "Unable to cancel build")
+ self.assertEqual(BuildStatus.BUILDING, pending_build.status)
+
def test_build_records_view(self):
# The BuildRecordsView can also be used to filter by architecture tag.
distroseries = self.factory.makeDistroSeries()
=== modified file 'lib/lp/soyuz/templates/build-index.pt'
--- lib/lp/soyuz/templates/build-index.pt 2011-01-19 22:35:43 +0000
+++ lib/lp/soyuz/templates/build-index.pt 2011-11-02 10:50:34 +0000
@@ -144,6 +144,9 @@
<tal:retry define="link context/menu:context/retry"
condition="link/enabled"
replace="structure link/fmt:link" />
+ <tal:cancel define="link context/menu:context/cancel"
+ condition="link/enabled"
+ replace="structure link/fmt:link" />
</p>
<ul>