← Back to team overview

launchpad-reviewers team mailing list archive

lp:~allenap/launchpad/dd-initseries-bug-727105-architecture-picker into lp:launchpad

 

Gavin Panella has proposed merging lp:~allenap/launchpad/dd-initseries-bug-727105-architecture-picker into lp:launchpad with lp:~allenap/launchpad/dd-initseries-bug-727105-derivation-vocab as a prerequisite.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #739234 in Launchpad itself: "+initseries: architectures picker"
  https://bugs.launchpad.net/launchpad/+bug/739234

For more details, see:
https://code.launchpad.net/~allenap/launchpad/dd-initseries-bug-727105-architecture-picker/+merge/54173

Updates test.css to import sam/skin.css so that javascript unittests
are readable.

Registers the view and a custom template for the +initseries
page. There is a very minimal test for this right now; the view is
trivially simple so there's not much else to do.

Exports DistroSeries.architectures, a collection of DistroArchSeries
related to the series.

Creates a new javascript widget, ArchitecturesCheckBoxListWidget, that
shows checkboxes for a given set of choices. This extends another new
widget, CheckBoxListWidget, that will itself probably be rebased in
subsequent branches (the form on that page is not yet complete). The
new architectures widget is also plumbed in. To try it out:

  - Go to https://launchpad.dev/ubuntu/hoary/+initseries

  - Change the options in the series drop-down box. Not all of them
    have any corresponding DistroArchSerieses.

-- 
https://code.launchpad.net/~allenap/launchpad/dd-initseries-bug-727105-architecture-picker/+merge/54173
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~allenap/launchpad/dd-initseries-bug-727105-architecture-picker into lp:launchpad.
=== modified file 'lib/canonical/launchpad/interfaces/_schema_circular_imports.py'
--- lib/canonical/launchpad/interfaces/_schema_circular_imports.py	2011-03-10 14:05:51 +0000
+++ lib/canonical/launchpad/interfaces/_schema_circular_imports.py	2011-03-21 09:34:34 +0000
@@ -58,6 +58,10 @@
     IBugTrackerComponentGroup,
     )
 from lp.bugs.interfaces.bugwatch import IBugWatch
+from lp.bugs.interfaces.structuralsubscription import (
+    IStructuralSubscription,
+    IStructuralSubscriptionTarget,
+    )
 from lp.buildmaster.enums import BuildStatus
 from lp.buildmaster.interfaces.buildfarmjob import IBuildFarmJob
 from lp.buildmaster.interfaces.buildqueue import IBuildQueue
@@ -100,10 +104,6 @@
 from lp.registry.interfaces.product import IProduct
 from lp.registry.interfaces.productseries import IProductSeries
 from lp.registry.interfaces.sourcepackage import ISourcePackage
-from lp.bugs.interfaces.structuralsubscription import (
-    IStructuralSubscription,
-    IStructuralSubscriptionTarget,
-    )
 from lp.services.comments.interfaces.conversation import IComment
 from lp.soyuz.enums import (
     PackagePublishingStatus,
@@ -389,6 +389,8 @@
     IDistroSeries, 'getDistroArchSeries', IDistroArchSeries)
 patch_reference_property(
     IDistroSeries, 'main_archive', IArchive)
+patch_collection_property(
+    IDistroSeries, 'architectures', IDistroArchSeries)
 patch_reference_property(
     IDistroSeries, 'distribution', IDistribution)
 patch_choice_parameter_type(

=== modified file 'lib/canonical/launchpad/javascript/test.css'
--- lib/canonical/launchpad/javascript/test.css	2010-10-10 21:54:16 +0000
+++ lib/canonical/launchpad/javascript/test.css	2011-03-21 09:34:34 +0000
@@ -1,3 +1,5 @@
+@import url("../icing/yui/assets/skins/sam/skin.css");
+
 /* Taken and customized from testlogger.css */
 .yui-console-entry-src { display:none; }
 

=== modified file 'lib/lp/registry/browser/configure.zcml'
--- lib/lp/registry/browser/configure.zcml	2011-01-24 20:53:10 +0000
+++ lib/lp/registry/browser/configure.zcml	2011-03-21 09:34:34 +0000
@@ -1953,6 +1953,8 @@
             name="+reassign"
             template="../../../canonical/launchpad/templates/object-reassignment.pt"/>
     </browser:pages>
+
+    <!-- DistroSeries create and initialize. -->
     <browser:page
         name="+addseries"
         for="lp.registry.interfaces.distribution.IDistribution"
@@ -1961,7 +1963,6 @@
         permission="launchpad.Admin"
         template="../../app/templates/generic-edit.pt">
     </browser:page>
-
     <browser:page
         name="+addseries"
         for="lp.registry.interfaces.distribution.IDerivativeDistribution"
@@ -1970,6 +1971,14 @@
         permission="launchpad.Append"
         template="../../app/templates/generic-edit.pt">
     </browser:page>
+    <browser:page
+        name="+initseries"
+        for="lp.registry.interfaces.distroseries.IDistroSeries"
+        class="lp.registry.browser.distroseries.DistroSeriesInitializeView"
+        facet="overview"
+        permission="launchpad.Admin"
+        template="../templates/distroseries-initialize.pt">
+    </browser:page>
 
     <browser:page
         for="lp.registry.interfaces.distribution.IDistribution"

=== modified file 'lib/lp/registry/browser/distroseries.py'
--- lib/lp/registry/browser/distroseries.py	2011-03-08 11:56:40 +0000
+++ lib/lp/registry/browser/distroseries.py	2011-03-21 09:34:34 +0000
@@ -11,10 +11,11 @@
     'DistroSeriesBreadcrumb',
     'DistroSeriesEditView',
     'DistroSeriesFacets',
+    'DistroSeriesInitializeView',
     'DistroSeriesLocalDifferences',
+    'DistroSeriesNavigation',
     'DistroSeriesPackageSearchView',
     'DistroSeriesPackagesView',
-    'DistroSeriesNavigation',
     'DistroSeriesView',
     ]
 
@@ -484,7 +485,7 @@
 
 
 class DistroSeriesAddView(LaunchpadFormView):
-    """A view to creat an `IDistrobutionSeries`."""
+    """A view to create an `IDistroSeries`."""
     schema = IDistroSeries
     field_names = [
         'name', 'displayname', 'title', 'summary', 'description', 'version',
@@ -517,6 +518,38 @@
         return canonical_url(self.context)
 
 
+class IDistroSeriesInitializeForm(Interface):
+
+    derived_from_series = Choice(
+        title=_('Derived from distribution series'),
+        default=None,
+        vocabulary="DistroSeriesDerivation",
+        description=_(
+            "Select the distribution series you "
+            "want to derive from."),
+        required=True)
+
+
+class DistroSeriesInitializeView(LaunchpadFormView):
+    """A view to initialize an `IDistroSeries`."""
+
+    schema = IDistroSeriesInitializeForm
+    field_names = [
+        "derived_from_series",
+        ]
+
+    custom_widget('derived_from_series', LaunchpadDropdownWidget)
+
+    label = 'Initialize series'
+    page_title = label
+
+    @property
+    def next_url(self):
+        return canonical_url(self.context)
+
+    cancel_url = next_url
+
+
 class DistroSeriesPackagesView(LaunchpadView):
     """A View to show series package to upstream package relationships."""
 
@@ -600,8 +633,7 @@
         for its own vocabulary, we set it up after all the others.
         """
         super(DistroSeriesLocalDifferences, self).setUpFields()
-        has_edit = check_permission('launchpad.Edit', self.context)
-
+        check_permission('launchpad.Edit', self.context)
         terms = [
             SimpleTerm(diff, diff.source_package_name.name,
                 diff.source_package_name.name)

=== added file 'lib/lp/registry/browser/tests/test_distroseries.py'
--- lib/lp/registry/browser/tests/test_distroseries.py	1970-01-01 00:00:00 +0000
+++ lib/lp/registry/browser/tests/test_distroseries.py	2011-03-21 09:34:34 +0000
@@ -0,0 +1,21 @@
+# Copyright 2011 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Tests for `lp.registry.browser.distroseries`."""
+
+__metaclass__ = type
+
+from canonical.testing.layers import DatabaseFunctionalLayer
+from lp.testing import TestCaseWithFactory
+from lp.testing.views import create_initialized_view
+
+
+class TestDistroSeriesInitializeView(TestCaseWithFactory):
+
+    layer = DatabaseFunctionalLayer
+
+    def test_init(self):
+        # There exists a +initseries view for distroseries.
+        distroseries = self.factory.makeDistroSeries()
+        view = create_initialized_view(distroseries, "+initseries")
+        self.assertTrue(view)

=== modified file 'lib/lp/registry/interfaces/distroseries.py'
--- lib/lp/registry/interfaces/distroseries.py	2011-03-10 14:05:51 +0000
+++ lib/lp/registry/interfaces/distroseries.py	2011-03-21 09:34:34 +0000
@@ -32,6 +32,7 @@
     webservice_error,
     )
 from lazr.restful.fields import (
+    CollectionField,
     Reference,
     ReferenceChoice,
     )
@@ -389,7 +390,11 @@
         """
 
     # DistroArchSeries lookup properties/methods.
-    architectures = Attribute("All architectures in this series.")
+    architectures = exported(
+        CollectionField(
+            title=_("All architectures in this series."),
+            value_type=Reference(schema=Interface), # IDistroArchSeries.
+            readonly=True))
 
     enabled_architectures = Attribute(
         "All architectures in this series with the 'enabled' flag set.")

=== added file 'lib/lp/registry/javascript/distroseries.js'
--- lib/lp/registry/javascript/distroseries.js	1970-01-01 00:00:00 +0000
+++ lib/lp/registry/javascript/distroseries.js	2011-03-21 09:34:34 +0000
@@ -0,0 +1,274 @@
+/**
+ * Copyright 2011 Canonical Ltd. This software is licensed under the
+ * GNU Affero General Public License version 3 (see the file LICENSE).
+ *
+ * DistroSeries related stuff.
+ *
+ * @module registry
+ * @submodule distroseries
+ */
+
+YUI.add('lp.registry.distroseries.initseries', function(Y) {
+
+Y.log('loading lp.registry.distroseries.initseries');
+
+var namespace = Y.namespace('lp.registry.distroseries.initseries');
+
+
+/**
+ * A form row matching that which LaunchpadForm presents, containing a
+ * list of checkboxes, and an optional label and description.
+ *
+ * @class CheckBoxListWidget
+ */
+var CheckBoxListWidget = function() {
+    CheckBoxListWidget.superclass.constructor.apply(this, arguments);
+};
+
+Y.mix(CheckBoxListWidget, {
+
+    NAME: 'checkBoxListWidget',
+
+    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
+         */
+        choices: {
+            getter: function() {
+                return this.fieldNode.all("li > input").get("value");
+            },
+            setter: function(value, name) {
+                var choices = Y.Array.unique(value).sort();
+                var field_name = this.get("name");
+                var list = Y.Node.create("<ul />");
+                choices.forEach(
+                    function(choice) {
+                        var item = Y.Node.create(
+                            "<li><input /> <label /></li>");
+                        item.one("input")
+                            .set("type", "checkbox")
+                            .set("name", field_name)
+                            .set("value", choice);
+                        item.one("label")
+                            .setAttribute(
+                                "for", item.one("input").generateID())
+                            .setStyle("font-weight", "normal")
+                            .set("text", choice);
+                        list.append(item);
+                    }
+                );
+                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);
+    }
+
+});
+
+namespace.CheckBoxListWidget = CheckBoxListWidget;
+
+
+/**
+ * A special form of CheckBoxListWidget for choosing architecture tags.
+ *
+ * @class ArchitecturesCheckBoxListWidget
+ */
+var ArchitecturesCheckBoxListWidget = function() {
+    ArchitecturesCheckBoxListWidget
+        .superclass.constructor.apply(this, arguments);
+};
+
+Y.mix(ArchitecturesCheckBoxListWidget, {
+
+    NAME: 'architecturesCheckBoxListWidget',
+
+    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 path = value + "/architectures";
+                var on = {
+                    start: Y.bind(this.showSpinner, this),
+                    success: Y.bind(this.set, this, "distroArchSerieses"),
+                    failure: this.error_handler.getFailureHandler(),
+                    end: Y.bind(this.hideSpinner, this)
+                };
+                this.client.get(path, {on: on});
+            }
+        },
+
+        /**
+         * The DistroArchSerieses the choices in this field should
+         * reflect. Takes the form of a Y.lp.client.Collection.
+         *
+         * @property distroArchSerieses
+         */
+        distroArchSerieses: {
+            setter: function(value, name) {
+                this.set(
+                    "choices", value.entries.map(
+                        function(das) {
+                            return das.get("architecture_tag");
+                        }
+                    )
+                );
+                if (value.entries.length == 0) {
+                    this.fieldNode.append(
+                        Y.Node.create('<p />').set(
+                            "text", "The chosen series has no architectures!"));
+                }
+                Y.lazr.anim.green_flash({node: this.fieldNode}).run();
+            }
+        }
+    }
+
+});
+
+Y.extend(ArchitecturesCheckBoxListWidget, CheckBoxListWidget, {
+
+    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);
+        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();
+    }
+
+});
+
+namespace.ArchitecturesCheckBoxListWidget = ArchitecturesCheckBoxListWidget;
+
+
+/**
+ * Setup the widgets on the +initseries page.
+ *
+ * @function setup
+ */
+namespace.setup = function() {
+    var form_container = Y.one("#init-series-form-container");
+    var form_table_body = form_container.one("table.form > tbody");
+    var architecture_choice = new ArchitecturesCheckBoxListWidget()
+        .set("name", "field.architectures")
+        .set("label", "Architectures")
+        .set("description", (
+                 "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 field_derived_from_series =
+        form_table_body.one("[name=field.derived_from_series]");
+    function update_architecture_choice() {
+        architecture_choice
+            .set("distroSeries", field_derived_from_series.get("value"));
+    }
+    field_derived_from_series.on("change", update_architecture_choice);
+
+    // Update the architectures widget for the selected distroseries.
+    update_architecture_choice();
+
+    // Show the form.
+    form_container.removeClass("unseen");
+};
+
+
+}, "0.1", {"requires": ["node", "dom", "io", "widget", "lp.client",
+                        "lazr.anim", "array-extras"]});

=== added file 'lib/lp/registry/javascript/tests/test_distroseries.html'
--- lib/lp/registry/javascript/tests/test_distroseries.html	1970-01-01 00:00:00 +0000
+++ lib/lp/registry/javascript/tests/test_distroseries.html	2011-03-21 09:34:34 +0000
@@ -0,0 +1,30 @@
+<!DOCTYPE
+   HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
+   "http://www.w3.org/TR/html4/strict.dtd";>
+<html>
+  <head>
+    <title>Launchpad DistroSeries</title>
+    <!-- YUI 3.0 Setup -->
+    <script type="text/javascript"
+            src="../../../../canonical/launchpad/icing/yui/yui/yui.js"></script>
+    <script type="text/javascript"
+            src="../../../../canonical/launchpad/icing/lazr/build/lazr.js"></script>
+    <link rel="stylesheet"
+          href="../../../../canonical/launchpad/icing/yui/cssreset/reset.css"/>
+    <link rel="stylesheet"
+          href="../../../../canonical/launchpad/icing/yui/cssfonts/fonts.css"/>
+    <link rel="stylesheet"
+          href="../../../../canonical/launchpad/icing/yui/cssbase/base.css"/>
+    <link rel="stylesheet"
+          href="../../../../canonical/launchpad/javascript/test.css" />
+    <!-- Required modules -->
+    <script type="text/javascript" src="../../../app/javascript/client.js"></script>
+    <!-- The module under test -->
+    <script type="text/javascript" src="../distroseries.js"></script>
+    <!-- The test suite -->
+    <script type="text/javascript" src="test_distroseries.js"></script>
+  </head>
+  <body class="yui3-skin-sam">
+    <div id="log"></div>
+  </body>
+</html>

=== added file 'lib/lp/registry/javascript/tests/test_distroseries.js'
--- lib/lp/registry/javascript/tests/test_distroseries.js	1970-01-01 00:00:00 +0000
+++ lib/lp/registry/javascript/tests/test_distroseries.js	2011-03-21 09:34:34 +0000
@@ -0,0 +1,225 @@
+/* Copyright (c) 2011, Canonical Ltd. All rights reserved. */
+
+YUI({
+    base: '../../../../canonical/launchpad/icing/yui/',
+    filter: 'raw', combine: false, fetchCSS: false
+    }).use(
+        'test', 'console', 'lp.registry.distroseries.initseries',
+        function(Y) {
+
+    var Assert = Y.Assert;
+    var ArrayAssert = Y.ArrayAssert;
+
+    var suite = new Y.Test.Suite("distroseries.initseries Tests");
+    var initseries = Y.lp.registry.distroseries.initseries;
+    var console = new Y.Console({newestOnTop: false});
+    console.render('#log');
+
+    var testCheckBoxListWidget = {
+        name: 'TestCheckBoxListWidget',
+
+        setUp: function() {
+            this.container = Y.Node.create("<div />");
+            this.widget = new initseries.CheckBoxListWidget();
+        },
+
+        tearDown: function() {
+            this.container.remove();
+        },
+
+        testRender: function() {
+            this.widget.render(this.container);
+            Assert.isTrue(
+                this.container.contains(
+                    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.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"));
+        },
+
+        testRenderWithNameChange: function() {
+            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"));
+        },
+
+        testRenderLabel: function() {
+            this.widget.set("label", "Test label");
+            this.widget.render(this.container);
+            Assert.areEqual(
+                "Test label",
+                this.container.one("label").get("text"));
+        },
+
+        testRenderLabelChange: function() {
+            this.widget.set("label", "Test label");
+            this.widget.render(this.container);
+            this.widget.set("label", "Another label");
+            Assert.areEqual(
+                "Another label",
+                this.container.one("label").get("text"));
+        },
+
+        testRenderDescription: function() {
+            this.widget.set("description", "Test description.");
+            this.widget.render(this.container);
+            Assert.areEqual(
+                "Test description.",
+                this.container.one("p.formHelp").get("text"));
+        },
+
+        testRenderDescriptionChange: function() {
+            this.widget.set("description", "Test description.");
+            this.widget.render(this.container);
+            this.widget.set("description", "Another description.");
+            Assert.areEqual(
+                "Another description.",
+                this.container.one("p.formHelp").get("text"));
+        }
+
+    };
+
+    suite.add(new Y.Test.Case(testCheckBoxListWidget));
+
+    var testArchitecturesCheckBoxListWidget = {
+        name: 'TestArchitecturesCheckBoxListWidget',
+
+        setUp: function() {
+            this.container = Y.Node.create("<div />");
+            this.widget = new initseries.ArchitecturesCheckBoxListWidget();
+        },
+
+        tearDown: function() {
+            this.container.remove();
+        },
+
+        testSetDistroArchSeriesesUpdatesChoices: function() {
+            var distro_arch_serieses = [
+                {architecture_tag: "i386"},
+                {architecture_tag: "amd64"},
+                {architecture_tag: "i386"}
+            ];
+            var distro_arch_serieses_collection =
+                new Y.lp.client.Collection(
+                    null, {entries: distro_arch_serieses}, null);
+            this.widget.set(
+                "distroArchSerieses",
+                distro_arch_serieses_collection);
+            ArrayAssert.itemsAreEqual(
+                ["amd64", "i386"],
+                this.widget.get("choices"));
+        },
+
+        testSetDistroSeriesInitiatesIO: function() {
+            var io = false;
+            this.widget.client = {
+                get: function(path, config) {
+                    io = true;
+                    Assert.areEqual("ubuntu/hoary/architectures", path);
+                    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.");
+        },
+
+        testSetDistroSeriesUpdatesDistroArchSeries: function() {
+            var distro_arch_serieses = [
+                {architecture_tag: "i386"},
+                {architecture_tag: "amd64"},
+                {architecture_tag: "i386"}
+            ];
+            var distro_arch_serieses_collection =
+                new Y.lp.client.Collection(
+                    null, {entries: distro_arch_serieses}, null);
+            this.widget.client = {
+                get: function(path, config) {
+                    config.on.success(distro_arch_serieses_collection);
+                }
+            };
+            this.widget.set("distroSeries", "ubuntu/hoary");
+            ArrayAssert.itemsAreEqual(
+                ["amd64", "i386"],
+                this.widget.get("choices"));
+        },
+
+        testSetDistroSeriesSpinner: function() {
+            var widget = this.widget;
+            widget.client = {
+                get: function(path, config) {
+                    Assert.isFalse(
+                        widget.fieldNode.contains(widget.spinner));
+                    config.on.start();
+                    Assert.isTrue(
+                        widget.fieldNode.contains(widget.spinner));
+                    config.on.end();
+                    Assert.isFalse(
+                        widget.fieldNode.contains(widget.spinner));
+                }
+            };
+            this.widget.set("distroSeries", "ubuntu/hoary");
+        },
+
+        testSetDistroSeriesError: function() {
+            var widget = this.widget;
+            widget.client = {
+                get: function(path, 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");
+        }
+
+    };
+
+    testArchitecturesCheckBoxListWidget = Y.merge(
+        testCheckBoxListWidget, testArchitecturesCheckBoxListWidget);
+
+    suite.add(new Y.Test.Case(testArchitecturesCheckBoxListWidget));
+
+    Y.Test.Runner.add(suite);
+
+    Y.on('domready', function() {
+        Y.Test.Runner.run();
+    });
+});

=== modified file 'lib/lp/registry/stories/webservice/xx-distroseries.txt'
--- lib/lp/registry/stories/webservice/xx-distroseries.txt	2011-01-26 19:35:17 +0000
+++ lib/lp/registry/stories/webservice/xx-distroseries.txt	2011-03-21 09:34:34 +0000
@@ -61,6 +61,7 @@
     active_milestones_collection_link:
         u'http://.../ubuntu/hoary/active_milestones'
     all_milestones_collection_link: u'http://.../ubuntu/hoary/all_milestones'
+    architectures_collection_link: u'http://.../ubuntu/hoary/architectures'
     bug_reported_acknowledgement: None
     bug_reporting_guidelines: None
     changeslist: u'hoary-changes@xxxxxxxxxx'
@@ -86,6 +87,7 @@
     version: u'5.04'
     web_link: u'http://launchpad.../ubuntu/hoary'
 
+
 Creating a milestone on the distroseries
 ----------------------------------------
 

=== added file 'lib/lp/registry/templates/distroseries-initialize.pt'
--- lib/lp/registry/templates/distroseries-initialize.pt	1970-01-01 00:00:00 +0000
+++ lib/lp/registry/templates/distroseries-initialize.pt	2011-03-21 09:34:34 +0000
@@ -0,0 +1,34 @@
+<html
+    xmlns="http://www.w3.org/1999/xhtml";
+    xmlns:tal="http://xml.zope.org/namespaces/tal";
+    xmlns:metal="http://xml.zope.org/namespaces/metal";
+    xmlns:i18n="http://xml.zope.org/namespaces/i18n";
+    metal:use-macro="view/macro:page/main_only"
+    i18n:domain="launchpad">
+  <metal:head-epilogue fill-slot="head_epilogue">
+    <style type="text/css">
+      .yui3-js-enabled .javascript-disabled { display: none; }
+    </style>
+  </metal:head-epilogue>
+  <body>
+    <div metal:fill-slot="main">
+      <div class="top-portlet">
+        This page allows you to initialize a distribution series.
+      </div>
+      <p class="error message javascript-disabled">
+        Javascript is required to use this page. Please enable
+        Javascript in your browser and reload this page. Alternatively,
+        please use the <code>deriveDistroSeries</code> API call via the
+        web service.
+      </p>
+      <div class="unseen" id="init-series-form-container">
+        <metal:form use-macro="context/@@launchpad_form/form" />
+      </div>
+      <script type="text/javascript">
+        LPS.use('lp.registry.distroseries.initseries', function(Y) {
+          Y.on('domready', Y.lp.registry.distroseries.initseries.setup);
+        });
+      </script>
+    </div>
+  </body>
+</html>