launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #03038
lp:~allenap/launchpad/dd-initseries-bug-727105-packageset-picker into lp:launchpad
Gavin Panella has proposed merging lp:~allenap/launchpad/dd-initseries-bug-727105-packageset-picker into lp:launchpad with lp:~allenap/launchpad/dd-initseries-bug-727105-architecture-picker as a prerequisite.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~allenap/launchpad/dd-initseries-bug-727105-packageset-picker/+merge/54382
More work on the +initseries page for deriving a distroseries:
- Much of CheckBoxListWidget has been refactored into a new superclass
FormRowWidget.
- There's a new widget, SelectWidget, another subclass of
FormRowWidget, for displaying a select areas.
- There's another new widget PackagesetPickerWidget, a subclass of
SelectWidget, for choosing packagesets to copy over to the derived
series.
- IPackagesetSet.getBySeries() was created and exported to support the
PackagesetPickerWidget.
This merge proposal is quite large, but a lot of it is Javascript
boilerplate-like code.
--
https://code.launchpad.net/~allenap/launchpad/dd-initseries-bug-727105-packageset-picker/+merge/54382
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~allenap/launchpad/dd-initseries-bug-727105-packageset-picker into lp:launchpad.
=== modified file 'lib/lp/registry/javascript/distroseries.js'
--- lib/lp/registry/javascript/distroseries.js 2011-03-22 16:48:34 +0000
+++ lib/lp/registry/javascript/distroseries.js 2011-03-22 16:48:38 +0000
@@ -17,6 +17,120 @@
/**
* A form row matching that which LaunchpadForm presents, containing a
+ * field (defined in a subclass), and an optional label and
+ * description.
+ *
+ * @class FormRowWidget
+ */
+var FormRowWidget = function() {
+ FormRowWidget.superclass.constructor.apply(this, arguments);
+};
+
+Y.mix(FormRowWidget, {
+
+ NAME: 'formRowWidget',
+
+ ATTRS: {
+
+ /**
+ * The field name.
+ *
+ * @property name
+ */
+ name: {
+ setter: function(value, name) {
+ this.fieldNode.all("input, select").set("name", value);
+ }
+ },
+
+ /**
+ * The top label for the field.
+ *
+ * @property label
+ */
+ label: {
+ getter: function() {
+ return this.labelNode.get("text");
+ },
+ setter: function(value, name) {
+ this.labelNode.set("text", value);
+ }
+ },
+
+ /**
+ * A description shown near the field.
+ *
+ * @label description
+ */
+ description: {
+ getter: function() {
+ return this.descriptionNode.get("text");
+ },
+ setter: function(value, name) {
+ this.descriptionNode.set("text", value);
+ }
+ }
+ }
+
+});
+
+Y.extend(FormRowWidget, Y.Widget, {
+
+ BOUNDING_TEMPLATE: "<tr></tr>",
+
+ CONTENT_TEMPLATE: '<td colspan="2"></td>',
+
+ initializer: function(config) {
+ this.labelNode = Y.Node.create("<label />");
+ this.fieldNode = Y.Node.create("<div></div>");
+ this.descriptionNode = Y.Node.create('<p class="formHelp" />');
+ this.spinnerNode = Y.Node.create(
+ '<img src="/@@/spinner" alt="Loading..." />');
+ },
+
+ renderUI: function() {
+ this.get("contentBox")
+ .append(this.labelNode)
+ .append(this.fieldNode)
+ .append(this.descriptionNode);
+ },
+
+ /**
+ * Show the spinner.
+ *
+ * @method showSpinner
+ */
+ showSpinner: function() {
+ this.fieldNode.empty().append(this.spinnerNode);
+ },
+
+ /**
+ * Hide the spinner.
+ *
+ * @method hideSpinner
+ */
+ hideSpinner: function() {
+ this.spinnerNode.remove();
+ },
+
+ /**
+ * Display an error.
+ *
+ * @method showError
+ */
+ showError: function(error) {
+ var message = Y.Node.create('<p />').set("text", error);
+ this.fieldNode.empty().append(message);
+ Y.lazr.anim.red_flash({node: message}).run();
+ }
+
+});
+
+namespace.FormRowWidget = FormRowWidget;
+
+
+/**
+ * A form row matching that which LaunchpadForm presents, containing a
* list of checkboxes, and an optional label and description.
*
* @class CheckBoxListWidget
@@ -32,17 +146,6 @@
ATTRS: {
/**
- * The field name.
- *
- * @property name
- */
- name: {
- setter: function(value, name) {
- this.fieldNode.all("input").set("name", value);
- }
- },
-
- /**
* An array of strings from which to choose.
*
* @property choices
@@ -73,59 +176,13 @@
);
this.fieldNode.empty().append(list);
}
- },
-
- /**
- * The top label for the field.
- *
- * @property label
- */
- label: {
- getter: function() {
- return this.labelNode.get("text");
- },
- setter: function(value, name) {
- this.labelNode.set("text", value);
- }
- },
-
- /**
- * A description shown near the field.
- *
- * @label description
- */
- description: {
- getter: function() {
- return this.descriptionNode.get("text");
- },
- setter: function(value, name) {
- this.descriptionNode.set("text", value);
- }
}
- }
-
-});
-
-Y.extend(CheckBoxListWidget, Y.Widget, {
-
- BOUNDING_TEMPLATE: "<tr></tr>",
-
- CONTENT_TEMPLATE: '<td colspan="2"></td>',
-
- initializer: function(config) {
- this.labelNode = Y.Node.create("<label />");
- this.fieldNode = Y.Node.create("<div></div>");
- this.descriptionNode = Y.Node.create('<p class="formHelp" />');
- },
-
- renderUI: function() {
- this.get("contentBox")
- .append(this.labelNode)
- .append(this.fieldNode)
- .append(this.descriptionNode);
- }
-
-});
+
+ }
+
+});
+
+Y.extend(CheckBoxListWidget, FormRowWidget);
namespace.CheckBoxListWidget = CheckBoxListWidget;
@@ -180,10 +237,11 @@
}
)
);
- if (value.entries.length == 0) {
+ if (value.entries.length === 0) {
this.fieldNode.append(
Y.Node.create('<p />').set(
- "text", "The chosen series has no architectures!"));
+ "text",
+ "The chosen series has no architectures!"));
}
Y.lazr.anim.green_flash({node: this.fieldNode}).run();
}
@@ -199,37 +257,6 @@
this.error_handler = new Y.lp.client.ErrorHandler();
this.error_handler.clearProgressUI = Y.bind(this.hideSpinner, this);
this.error_handler.showError = Y.bind(this.showError, this);
- this.spinner = Y.Node.create(
- '<img src="/@@/spinner" alt="Loading..." />');
- },
-
- /**
- * Show the spinner.
- *
- * @method showSpinner
- */
- showSpinner: function() {
- this.fieldNode.empty().append(this.spinner);
- },
-
- /**
- * Hide the spinner.
- *
- * @method hideSpinner
- */
- hideSpinner: function() {
- this.spinner.remove();
- },
-
- /**
- * Display an error.
- *
- * @method showError
- */
- showError: function(error) {
- var message = Y.Node.create('<p />').set("text", error);
- this.fieldNode.empty().append(message);
- Y.lazr.anim.red_flash({node: message}).run();
}
});
@@ -238,6 +265,202 @@
/**
+ * A special form of FormRowWidget, containing a select control.
+ *
+ * @class SelectWidget
+ */
+var SelectWidget = function() {
+ SelectWidget.superclass.constructor.apply(this, arguments);
+};
+
+Y.mix(SelectWidget, {
+
+ NAME: 'selectWidget',
+
+ ATTRS: {
+
+ /**
+ * An array of objects from which to choose. Each object
+ * should contain a value for "value", "text" and "data".
+ *
+ * @property choices
+ */
+ choices: {
+ getter: function() {
+ /* I think this is a YUI3 wart; I can't see any way to
+ map() over a NodeList, so I must push the elements
+ one by one into an array first. */
+ var options = Y.Array([]);
+ this.fieldNode.all("select > option").each(
+ function(option) { options.push(option); });
+ return options.map(
+ function(option) {
+ return {
+ value: option.get("value"),
+ text: option.get("text"),
+ data: option.getData("data")
+ };
+ }
+ );
+ },
+ setter: function(value, name) {
+ var select = Y.Node.create("<select />");
+ select.set("name", this.get("name"))
+ .set("size", this.get("size"));
+ if (this.get("multiple")) {
+ select.set("multiple", "multiple");
+ }
+ var choices = Y.Array(value);
+ choices.forEach(
+ function(choice) {
+ var option = Y.Node.create("<option />");
+ option.set("value", choice.value)
+ .set("text", choice.text)
+ .setData("data", choice.data);
+ select.append(option);
+ }
+ );
+ if (choices.length > 0) {
+ this.fieldNode.empty().append(select);
+ }
+ else {
+ this.fieldNode.empty();
+ }
+ }
+ },
+
+ /**
+ * The number of rows to show in the select widget.
+ *
+ * @property size
+ */
+ size: {
+ value: 1,
+ setter: function(value, name) {
+ this.fieldNode.all("select").set("size", value);
+ }
+ },
+
+ /**
+ * Whether multiple rows can be selected.
+ *
+ * @property multiple
+ */
+ multiple: {
+ value: false,
+ setter: function(value, name) {
+ var select = this.fieldNode.all("select");
+ if (value) {
+ select.setAttribute("multiple", "multiple");
+ return true;
+ }
+ else {
+ select.removeAttribute("multiple");
+ return false;
+ }
+ }
+ }
+
+ }
+
+});
+
+Y.extend(SelectWidget, FormRowWidget);
+
+namespace.SelectWidget = SelectWidget;
+
+
+/**
+ * A special form of SelectWidget for choosing packagesets.
+ *
+ * @class PackagesetPickerWidget
+ */
+var PackagesetPickerWidget = function() {
+ PackagesetPickerWidget
+ .superclass.constructor.apply(this, arguments);
+};
+
+Y.mix(PackagesetPickerWidget, {
+
+ NAME: 'packagesetPickerWidget',
+
+ ATTRS: {
+
+ /**
+ * The DistroSeries the choices in this field should
+ * reflect. Takes the form of a string, e.g. "ubuntu/hoary".
+ *
+ * @property distroSeries
+ */
+ distroSeries: {
+ setter: function(value, name) {
+ var distro_series_uri = Y.lp.client.get_absolute_uri(value);
+ var on = {
+ start: Y.bind(this.showSpinner, this),
+ success: Y.bind(this.set, this, "packageSets"),
+ failure: this.error_handler.getFailureHandler(),
+ end: Y.bind(this.hideSpinner, this)
+ };
+ var config = {
+ on: on,
+ parameters: {
+ distroseries: distro_series_uri
+ }
+ };
+ this.client.named_get("package-sets", "getBySeries", config);
+ }
+ },
+
+ /**
+ * The Packagesets the choices in this field should
+ * reflect. Takes the form of a Y.lp.client.Collection.
+ *
+ * @property packageSets
+ */
+ packageSets: {
+ setter: function(value, name) {
+ this.set(
+ "choices", value.entries.map(
+ function(packageset) {
+ return {
+ data: packageset,
+ value: packageset.get("name"),
+ text: (
+ packageset.get("name") + ": " +
+ packageset.get("description"))
+ };
+ }
+ )
+ );
+ if (value.entries.length === 0) {
+ this.fieldNode.append(
+ Y.Node.create('<p />').set(
+ "text",
+ "The chosen series has no package sets!"));
+ }
+ Y.lazr.anim.green_flash({node: this.fieldNode}).run();
+ }
+ }
+
+ }
+
+});
+
+Y.extend(PackagesetPickerWidget, SelectWidget, {
+
+ initializer: function(config) {
+ this.client = new Y.lp.client.Launchpad();
+ this.error_handler = new Y.lp.client.ErrorHandler();
+ this.error_handler.clearProgressUI = Y.bind(this.hideSpinner, this);
+ this.error_handler.showError = Y.bind(this.showError, this);
+ }
+
+});
+
+namespace.PackagesetPickerWidget = PackagesetPickerWidget;
+
+
+/**
* Setup the widgets on the +initseries page.
*
* @function setup
@@ -252,10 +475,19 @@
"Choose the architectures you want to " +
"use from the parent series."))
.render(form_table_body);
-
- // Wire up the distroseries select to the architectures widget.
+ var packageset_choice = new PackagesetPickerWidget()
+ .set("name", "field.packagesets")
+ .set("size", 5)
+ .set("multiple", true)
+ .set("label", "Package sets to copy from parent")
+ .set("description", (
+ "The package sets that will be imported " +
+ "into the derived distroseries."))
+ .render(form_table_body);
var field_derived_from_series =
form_table_body.one("[name=field.derived_from_series]");
+
+ // Wire up the distroseries select to the architectures widget.
function update_architecture_choice() {
architecture_choice
.set("distroSeries", field_derived_from_series.get("value"));
@@ -265,6 +497,16 @@
// Update the architectures widget for the selected distroseries.
update_architecture_choice();
+ // Wire up the distroseries select to the packagesets widget.
+ function update_packageset_choice() {
+ packageset_choice
+ .set("distroSeries", field_derived_from_series.get("value"));
+ }
+ field_derived_from_series.on("change", update_packageset_choice);
+
+ // Update the packagesets widget for the selected distroseries.
+ update_packageset_choice();
+
// Show the form.
form_container.removeClass("unseen");
};
=== modified file 'lib/lp/registry/javascript/tests/test_distroseries.js'
--- lib/lp/registry/javascript/tests/test_distroseries.js 2011-03-22 16:48:34 +0000
+++ lib/lp/registry/javascript/tests/test_distroseries.js 2011-03-22 16:48:38 +0000
@@ -15,12 +15,24 @@
var console = new Y.Console({newestOnTop: false});
console.render('#log');
- var testCheckBoxListWidget = {
- name: 'TestCheckBoxListWidget',
+ var attrgetter = function(name) {
+ return function(thing) {
+ return thing[name];
+ };
+ };
+
+ var attrselect = function(name) {
+ return function(things) {
+ return Y.Array(things).map(attrgetter(name));
+ };
+ };
+
+ var testFormRowWidget = {
+ name: 'TestFormRowWidget',
setUp: function() {
this.container = Y.Node.create("<div />");
- this.widget = new initseries.CheckBoxListWidget();
+ this.widget = new initseries.FormRowWidget();
},
tearDown: function() {
@@ -34,46 +46,25 @@
this.widget.get("boundingBox")));
},
- testRenderChoices: function() {
- this.widget.set("choices", ["a", "b"]);
- this.widget.render(this.container);
- ArrayAssert.itemsAreEqual(
- ["a", "b"],
- this.container.all("li > input").get("value"));
- ArrayAssert.itemsAreEqual(
- ["a", "b"],
- this.container.all("li > label").get("text"));
- },
-
- testRenderChoicesChange: function() {
- this.widget.set("choices", ["a", "b"]);
- this.widget.render(this.container);
- this.widget.set("choices", ["c", "d", "e"]);
- ArrayAssert.itemsAreEqual(
- ["c", "d", "e"],
- this.container.all("li > input").get("value"));
- ArrayAssert.itemsAreEqual(
- ["c", "d", "e"],
- this.container.all("li > label").get("text"));
- },
-
testRenderWithName: function() {
+ this.widget.fieldNode.append(
+ Y.Node.create("<input /><input />"));
this.widget.set("name", "field");
- this.widget.set("choices", ["a", "b"]);
this.widget.render(this.container);
ArrayAssert.itemsAreEqual(
["field", "field"],
- this.container.all("li > input").get("name"));
+ this.container.all("input").get("name"));
},
testRenderWithNameChange: function() {
+ this.widget.fieldNode.append(
+ Y.Node.create("<input /><input />"));
this.widget.set("name", "field");
- this.widget.set("choices", ["a", "b"]);
this.widget.render(this.container);
this.widget.set("name", "plain");
ArrayAssert.itemsAreEqual(
["plain", "plain"],
- this.container.all("li > input").get("name"));
+ this.container.all("input").get("name"));
},
testRenderLabel: function() {
@@ -108,10 +99,69 @@
Assert.areEqual(
"Another description.",
this.container.one("p.formHelp").get("text"));
- }
-
- };
-
+ },
+
+ testSpinner: function() {
+ Assert.isFalse(
+ this.widget.fieldNode.contains(this.widget.spinnerNode));
+ this.widget.showSpinner();
+ Assert.isTrue(
+ this.widget.fieldNode.contains(this.widget.spinnerNode));
+ this.widget.hideSpinner();
+ Assert.isFalse(
+ this.widget.fieldNode.contains(this.widget.spinnerNode));
+ },
+
+ testShowError: function() {
+ this.widget.showError("Unrealistic expectations.");
+ Assert.areEqual(
+ "Unrealistic expectations.",
+ this.widget.fieldNode.one("p").get("text"));
+ }
+
+ };
+
+ suite.add(new Y.Test.Case(testFormRowWidget));
+
+ var testCheckBoxListWidget = {
+ name: 'TestCheckBoxListWidget',
+
+ setUp: function() {
+ this.container = Y.Node.create("<div />");
+ this.widget = new initseries.CheckBoxListWidget();
+ },
+
+ tearDown: function() {
+ this.container.remove();
+ },
+
+ testRenderChoices: function() {
+ this.widget.set("choices", ["a", "b"]);
+ this.widget.render(this.container);
+ ArrayAssert.itemsAreEqual(
+ ["a", "b"],
+ this.container.all("li > input").get("value"));
+ ArrayAssert.itemsAreEqual(
+ ["a", "b"],
+ this.container.all("li > label").get("text"));
+ },
+
+ testRenderChoicesChange: function() {
+ this.widget.set("choices", ["a", "b"]);
+ this.widget.render(this.container);
+ this.widget.set("choices", ["c", "d", "e"]);
+ ArrayAssert.itemsAreEqual(
+ ["c", "d", "e"],
+ this.container.all("li > input").get("value"));
+ ArrayAssert.itemsAreEqual(
+ ["c", "d", "e"],
+ this.container.all("li > label").get("text"));
+ }
+
+ };
+
+ testCheckBoxListWidget = Y.merge(
+ testFormRowWidget, testCheckBoxListWidget);
suite.add(new Y.Test.Case(testCheckBoxListWidget));
var testArchitecturesCheckBoxListWidget = {
@@ -183,13 +233,13 @@
widget.client = {
get: function(path, config) {
Assert.isFalse(
- widget.fieldNode.contains(widget.spinner));
+ widget.fieldNode.contains(widget.spinnerNode));
config.on.start();
Assert.isTrue(
- widget.fieldNode.contains(widget.spinner));
+ widget.fieldNode.contains(widget.spinnerNode));
config.on.end();
Assert.isFalse(
- widget.fieldNode.contains(widget.spinner));
+ widget.fieldNode.contains(widget.spinnerNode));
}
};
this.widget.set("distroSeries", "ubuntu/hoary");
@@ -214,9 +264,281 @@
testArchitecturesCheckBoxListWidget = Y.merge(
testCheckBoxListWidget, testArchitecturesCheckBoxListWidget);
-
suite.add(new Y.Test.Case(testArchitecturesCheckBoxListWidget));
+ var testSelectWidget = {
+ name: 'TestSelectWidget',
+
+ setUp: function() {
+ this.container = Y.Node.create("<div />");
+ this.widget = new initseries.SelectWidget();
+ },
+
+ tearDown: function() {
+ this.container.remove();
+ },
+
+ testNameChange: function() {
+ var choices = [
+ {value: "a", text: "A", data: 123},
+ {value: "b", text: "B", data: 456},
+ {value: "c", text: "C", data: 789}
+ ];
+ this.widget
+ .set("name", "foo")
+ .set("choices", choices);
+ var select = this.widget.fieldNode.one("select");
+ Assert.areEqual("foo", select.get("name"));
+ this.widget
+ .set("name", "bar");
+ Assert.areEqual("bar", select.get("name"));
+ },
+
+ testChoices: function() {
+ var choices = [
+ {value: "a", text: "A", data: 123},
+ {value: "b", text: "B", data: 456},
+ {value: "c", text: "C", data: 789}
+ ];
+ this.widget.set("choices", choices);
+ var choices_observed = this.widget.get("choices");
+ /* We have to compare bit by bit ourselves because
+ Javascript is a language born in hell. */
+ ArrayAssert.itemsAreEqual(
+ attrselect("value")(choices),
+ attrselect("value")(choices_observed));
+ ArrayAssert.itemsAreEqual(
+ attrselect("text")(choices),
+ attrselect("text")(choices_observed));
+ ArrayAssert.itemsAreEqual(
+ attrselect("data")(choices),
+ attrselect("data")(choices_observed));
+ },
+
+ testRenderChoices: function() {
+ var choices = [
+ {value: "a", text: "A", data: 123},
+ {value: "b", text: "B", data: 456},
+ {value: "c", text: "C", data: 789}
+ ];
+ this.widget.set("choices", choices);
+ this.widget.render(this.container);
+ ArrayAssert.itemsAreEqual(
+ ["a", "b", "c"],
+ this.container.all("select > option").get("value"));
+ ArrayAssert.itemsAreEqual(
+ ["A", "B", "C"],
+ this.container.all("select > option").get("text"));
+ },
+
+ testRenderEmptyChoices: function() {
+ this.widget.fieldNode.append("something");
+ this.widget.set("choices", []);
+ this.widget.render(this.container);
+ Assert.isNull(this.container.one("select"));
+ Assert.isFalse(this.widget.fieldNode.hasChildNodes());
+ },
+
+ testRenderChoicesChange: function() {
+ var choices1 = [
+ {value: "a", text: "A", data: 123}
+ ];
+ this.widget.set("choices", choices1);
+ this.widget.render(this.container);
+ var choices2 = [
+ {value: "b", text: "B", data: 456},
+ {value: "c", text: "C", data: 789}
+ ];
+ this.widget.set("choices", choices2);
+ ArrayAssert.itemsAreEqual(
+ ["b", "c"],
+ this.container.all("select > option").get("value"));
+ ArrayAssert.itemsAreEqual(
+ ["B", "C"],
+ this.container.all("select > option").get("text"));
+ },
+
+ testSize: function() {
+ Assert.areEqual(1, this.widget.get("size"));
+ },
+
+ testRenderSize: function() {
+ var choices = [
+ {value: "a", text: "A", data: 123},
+ {value: "b", text: "B", data: 456},
+ {value: "c", text: "C", data: 789}
+ ];
+ this.widget
+ .set("choices", choices)
+ .set("size", 7)
+ .render(this.container);
+ Assert.areEqual(
+ 7, this.widget.fieldNode.one("select").get("size"));
+ },
+
+ testRenderSizeChange: function() {
+ var choices = [
+ {value: "a", text: "A", data: 123},
+ {value: "b", text: "B", data: 456},
+ {value: "c", text: "C", data: 789}
+ ];
+ this.widget
+ .set("choices", choices)
+ .set("size", 3)
+ .render(this.container)
+ .set("size", 5);
+ Assert.areEqual(
+ 5, this.widget.fieldNode.one("select").get("size"));
+ },
+
+ testMultiple: function() {
+ Assert.areEqual(false, this.widget.get("multiple"));
+ },
+
+ testRenderMultiple: function() {
+ var choices = [
+ {value: "a", text: "A", data: 123},
+ {value: "b", text: "B", data: 456},
+ {value: "c", text: "C", data: 789}
+ ];
+ this.widget
+ .set("choices", choices)
+ .set("multiple", true)
+ .render(this.container);
+ Assert.isTrue(
+ this.widget.fieldNode.one("select")
+ .hasAttribute("multiple"));
+ },
+
+ testRenderMultipleChange: function() {
+ var choices = [
+ {value: "a", text: "A", data: 123},
+ {value: "b", text: "B", data: 456},
+ {value: "c", text: "C", data: 789}
+ ];
+ this.widget
+ .set("choices", choices)
+ .set("multiple", true)
+ .render(this.container)
+ .set("multiple", false);
+ Assert.isFalse(
+ this.widget.fieldNode.one("select")
+ .hasAttribute("multiple"));
+ }
+
+ };
+
+ testSelectWidget = Y.merge(
+ testFormRowWidget, testSelectWidget);
+ suite.add(new Y.Test.Case(testSelectWidget));
+
+ var testPackagesetPickerWidget = {
+ name: 'TestPackagesetPickerWidget',
+
+ setUp: function() {
+ this.container = Y.Node.create("<div />");
+ this.widget = new initseries.PackagesetPickerWidget();
+ },
+
+ tearDown: function() {
+ this.container.remove();
+ },
+
+ testSetPackageSetsUpdatesChoices: function() {
+ var package_sets = [
+ {name: "foo", description: "Foo"},
+ {name: "bar", description: "Bar"},
+ {name: "baz", description: "Baz"}
+ ];
+ var package_sets_collection =
+ new Y.lp.client.Collection(
+ null, {entries: package_sets}, null);
+ this.widget.set(
+ "packageSets",
+ package_sets_collection);
+ var choices = this.widget.get("choices");
+ ArrayAssert.itemsAreEqual(
+ ["foo", "bar", "baz"],
+ attrselect("value")(choices));
+ },
+
+ testSetDistroSeriesInitiatesIO: function() {
+ var io = false;
+ this.widget.client = {
+ named_get: function(path, operation, config) {
+ io = true;
+ Assert.areEqual("package-sets", path);
+ Assert.areEqual("getBySeries", operation);
+ Assert.isNotNull(
+ config.parameters.distroseries.match(
+ new RegExp("/ubuntu/hoary$")));
+ Assert.isObject(config.on);
+ Assert.isFunction(config.on.success);
+ Assert.isFunction(config.on.failure);
+ }
+ };
+ this.widget.set("distroSeries", "ubuntu/hoary");
+ Assert.isTrue(io, "No IO initiated.");
+ },
+
+ testSetDistroSeriesUpdatesPackageSets: function() {
+ var package_sets = [
+ {name: "foo", description: "Foo"},
+ {name: "bar", description: "Bar"},
+ {name: "baz", description: "Baz"}
+ ];
+ var package_sets_collection =
+ new Y.lp.client.Collection(
+ null, {entries: package_sets}, null);
+ this.widget.client = {
+ named_get: function(path, operation, config) {
+ config.on.success(package_sets_collection);
+ }
+ };
+ this.widget.set("distroSeries", "ubuntu/hoary");
+ ArrayAssert.itemsAreEqual(
+ ["foo", "bar", "baz"],
+ attrselect("value")(this.widget.get("choices")));
+ },
+
+ testSetDistroSeriesSpinner: function() {
+ var widget = this.widget;
+ widget.client = {
+ named_get: function(path, operation, config) {
+ Assert.isFalse(
+ widget.fieldNode.contains(widget.spinnerNode));
+ config.on.start();
+ Assert.isTrue(
+ widget.fieldNode.contains(widget.spinnerNode));
+ config.on.end();
+ Assert.isFalse(
+ widget.fieldNode.contains(widget.spinnerNode));
+ }
+ };
+ this.widget.set("distroSeries", "ubuntu/hoary");
+ },
+
+ testSetDistroSeriesError: function() {
+ var widget = this.widget;
+ widget.client = {
+ named_get: function(path, operation, config) {
+ config.on.failure(
+ null, {status: 404,
+ responseText: "Not found"});
+ Assert.areEqual(
+ "Not found",
+ widget.fieldNode.one("p").get("text"));
+ }
+ };
+ this.widget.set("distroSeries", "ubuntu/hoary");
+ }
+
+ };
+
+ testPackagesetPickerWidget = Y.merge(
+ testSelectWidget, testPackagesetPickerWidget);
+ suite.add(new Y.Test.Case(testPackagesetPickerWidget));
+
Y.Test.Runner.add(suite);
Y.on('domready', function() {
=== modified file 'lib/lp/soyuz/configure.zcml'
--- lib/lp/soyuz/configure.zcml 2011-03-10 14:05:51 +0000
+++ lib/lp/soyuz/configure.zcml 2011-03-22 16:48:38 +0000
@@ -806,6 +806,7 @@
get
getByName
getByOwner
+ getBySeries
setsIncludingSource"/>
<require
permission="launchpad.Edit"
=== modified file 'lib/lp/soyuz/interfaces/packageset.py'
--- lib/lp/soyuz/interfaces/packageset.py 2011-03-06 06:47:27 +0000
+++ lib/lp/soyuz/interfaces/packageset.py 2011-03-22 16:48:38 +0000
@@ -425,6 +425,22 @@
"""
@operation_parameters(
+ distroseries=Reference(
+ IDistroSeries, title=_("Distroseries"), required=True,
+ readonly=True, description=_(
+ "The distribution series to which the packagesets "
+ "are related.")))
+ @operation_returns_collection_of(IPackageset)
+ @export_read_operation()
+ def getBySeries(distroseries):
+ """Return the package sets associated with the given distroseries.
+
+ :param distroseries: A `DistroSeries`.
+
+ :return: An iterable collection of `IPackageset` instances.
+ """
+
+ @operation_parameters(
sourcepackagename=TextLine(
title=_('Source package name'), required=True),
distroseries=copy_field(IPackageset['distroseries'], required=False),
@@ -454,4 +470,3 @@
def __getitem__(name):
"""Retrieve a package set by name."""
-
=== modified file 'lib/lp/soyuz/model/packageset.py'
--- lib/lp/soyuz/model/packageset.py 2011-03-05 09:57:00 +0000
+++ lib/lp/soyuz/model/packageset.py 2011-03-22 16:48:38 +0000
@@ -408,6 +408,13 @@
result_set = store.find(Packageset, Packageset.owner == owner)
return _order_result_set(result_set)
+ def getBySeries(self, distroseries):
+ """See `IPackagesetSet`."""
+ store = IStore(Packageset)
+ result_set = store.find(
+ Packageset, Packageset.distroseries == distroseries)
+ return _order_result_set(result_set)
+
def get(self):
"""See `IPackagesetSet`."""
store = IStore(Packageset)
=== modified file 'lib/lp/soyuz/stories/webservice/xx-packageset.txt'
--- lib/lp/soyuz/stories/webservice/xx-packageset.txt 2011-02-13 22:54:48 +0000
+++ lib/lp/soyuz/stories/webservice/xx-packageset.txt 2011-03-22 16:48:38 +0000
@@ -1,4 +1,5 @@
-= Package sets =
+Package sets
+------------
Package sets facilitate the grouping of packages for purposes like the
control of upload permissions, the calculation of build and runtime package
@@ -19,13 +20,13 @@
interested in package sets and the complete functionality they offer.
-== General package set properties ==
+General package set properties
+==============================
We start off by creating an 'umbrella' package set that will include all
source packages.
>>> from zope.component import getUtility
- >>> from lp.registry.interfaces.person import IPersonSet
>>> name12 = webservice.get("/~name12").jsonBody()
>>> response = webservice.named_post(
@@ -85,7 +86,7 @@
Populate the 'umbrella' package set with source packages.
>>> from canonical.launchpad.webapp.interfaces import (
- ... IStoreSelector, MAIN_STORE, DEFAULT_FLAVOR, MASTER_FLAVOR)
+ ... IStoreSelector, MAIN_STORE, DEFAULT_FLAVOR)
>>> from lp.registry.model.sourcepackagename import SourcePackageName
>>> store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
>>> all_spns = store.find(SourcePackageName)
@@ -216,7 +217,8 @@
...
-=== Package sets and distro series ===
+Package sets and distro series
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Every package set is associated with a distro series.
@@ -229,8 +231,21 @@
>>> print mozilla['self_link']
http://api.launchpad.dev/beta/package-sets/hoary/mozilla
-
-=== Related package sets ===
+A collection of package sets belonging to a given distro series can be
+obtained via the `getBySeries` call.
+
+ >>> packagesets = webservice.named_get(
+ ... '/package-sets', 'getBySeries', {},
+ ... distroseries=mozilla['distroseries_link']).jsonBody()
+ >>> for entry in packagesets["entries"]:
+ ... print "{entry[name]}: {entry[description]}".format(entry=entry)
+ gnome: Contains all gnome packages
+ mozilla: Contains all mozilla packages
+ umbrella: Contains all source packages
+
+
+Related package sets
+~~~~~~~~~~~~~~~~~~~~
When adding a package set we can specify that is to be related to another set
that exists already.
@@ -267,7 +282,8 @@
http://api.launchpad.dev/beta/package-sets/grumpy/mozilla
-== Package set hierarchy ==
+Package set hierarchy
+=====================
More package sets are needed to set up the hierarchy described below.
@@ -519,7 +535,8 @@
["cnews", "thunderbird"]
-== Archive permissions and package sets ==
+Archive permissions and package sets
+====================================
Operating on package set based archive permissions is possible via
the Launchpad API as well.
@@ -544,7 +561,6 @@
Let's see what we've got:
- >>> from lazr.restful.testing.webservice import pprint_entry
>>> new_permission = webservice.get(
... response.getHeader('Location')).jsonBody()
>>> pprint_entry(new_permission)
@@ -819,7 +835,8 @@
set listed as well.
-== Archive permission URLs ==
+Archive permission URLs
+=======================
Archive permissions can be accessed via their URLs in direct fashion.
=== modified file 'lib/lp/soyuz/tests/test_packageset.py'
--- lib/lp/soyuz/tests/test_packageset.py 2010-10-04 19:50:45 +0000
+++ lib/lp/soyuz/tests/test_packageset.py 2011-03-22 16:48:38 +0000
@@ -121,6 +121,20 @@
'kernel', distroseries=self.distroseries_experimental)
self.assertEqual(pset2, pset_found)
+ def test_get_by_distroseries(self):
+ # IPackagesetSet.getBySeries() will return those package sets
+ # associated with the given distroseries.
+ pset1 = self.packageset_set.new(
+ u'timmy', u'Timmy Mallett', self.person1)
+ pset2 = self.packageset_set.new(
+ u'savile', u'Jimmy Savile', self.person1)
+ self.packageset_set.new(
+ u'hoskins', u'Bob Hoskins', self.person1,
+ distroseries=self.distroseries_experimental)
+ self.assertContentEqual(
+ [pset1, pset2],
+ self.packageset_set.getBySeries(self.distroseries_current))
+
class TestPackageset(TestCaseWithFactory):
Follow ups