← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~bac/launchpad/904335-create-milestones-tags into lp:launchpad

 

Brad Crittenden has proposed merging lp:~bac/launchpad/904335-create-milestones-tags into lp:launchpad with lp:~bac/launchpad/904335-export-tags as a prerequisite.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #904335 in Launchpad itself: "Project groups need  a way to aggregate project milestones"
  https://bugs.launchpad.net/launchpad/+bug/904335

For more details, see:
https://code.launchpad.net/~bac/launchpad/904335-create-milestones-tags/+merge/87669

Add fields for include milestone tags when a new milestone is created.

Tests:
bin/test -vv lp.registry.javascript.tests.test_milestone_creation
-- 
https://code.launchpad.net/~bac/launchpad/904335-create-milestones-tags/+merge/87669
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~bac/launchpad/904335-create-milestones-tags into lp:launchpad.
=== modified file 'lib/lp/bugs/interfaces/bugtask.py'
--- lib/lp/bugs/interfaces/bugtask.py	2012-01-05 19:12:36 +0000
+++ lib/lp/bugs/interfaces/bugtask.py	2012-01-05 19:12:40 +0000
@@ -918,8 +918,7 @@
         """
 
     def userHasBugSupervisorPrivileges(user):
-        """Is the user a privledged one, allowed to changed details on a
-        bug?
+        """Is the user privileged and allowed to change details on a bug?
 
         :return: A boolean.
         """

=== modified file 'lib/lp/bugs/javascript/bug_tags_entry.js'
--- lib/lp/bugs/javascript/bug_tags_entry.js	2011-08-09 14:18:02 +0000
+++ lib/lp/bugs/javascript/bug_tags_entry.js	2012-01-05 19:12:40 +0000
@@ -46,9 +46,9 @@
     tag_list_span.all(A).each(function(anchor) {
         tags.push(anchor.get(INNER_HTML));
     });
-    
+
     var tag_list = tags.join(' ');
-    /* If there are tags then add a space to the end of the string so the user 
+    /* If there are tags then add a space to the end of the string so the user
        doesn't have to type one. */
     if (tag_list != "") {
         tag_list += ' ';
@@ -61,6 +61,13 @@
  */
 var base_url = window.location.href.split('/+bug')[0] + '/+bugs?field.tag=';
 
+namespace.parse_tags = function(tag_string) {
+    var tags  = Y.Array(
+        Y.Lang.trim(tag_string).split(new RegExp('\\s+'))).filter(
+            function(elem) { return elem !== ''; });
+    return tags;
+};
+
 /**
  * Save the currently entered tags and switch inline editing off.
  *
@@ -68,9 +75,7 @@
  */
 var save_tags = function() {
     var lp_client = new Y.lp.client.Launchpad();
-    var tags = Y.Array(
-        Y.Lang.trim(tag_input.get(VALUE)).split(new RegExp('\\s+'))).filter(
-            function(elem) { return elem !== ''; });
+    var tags = namespace.parse_tags(tag_input.get(VALUE));
     var bug = new Y.lp.client.Entry(
         lp_client, LP.cache[BUG], LP.cache[BUG].self_link);
     bug.removeAttr('http_etag');
@@ -243,4 +248,3 @@
 }, "0.1", {"requires": ["base", "io-base", "node", "substitute", "node-menunav",
                         "lazr.base", "lp.anim", "lazr.autocomplete",
                         "lp.client"]});
-

=== added file 'lib/lp/bugs/javascript/tests/test_bug_tags_entry.html'
--- lib/lp/bugs/javascript/tests/test_bug_tags_entry.html	1970-01-01 00:00:00 +0000
+++ lib/lp/bugs/javascript/tests/test_bug_tags_entry.html	2012-01-05 19:12:40 +0000
@@ -0,0 +1,25 @@
+<html>
+  <head>
+    <title>Launchpad bug tags entry</title>
+    <!-- YUI and test setup -->
+    <script type="text/javascript"
+          src="../../../../canonical/launchpad/icing/yui/yui/yui.js">
+    </script>
+    <link rel="stylesheet" href="../../../app/javascript/testing/test.css" />
+    <script type="text/javascript"
+          src="../../../app/javascript/testing/testrunner.js"></script>
+
+    <!-- Other dependencies -->
+    <script type="text/javascript" src="../../../app/javascript/lp.js"></script>
+    <script type="text/javascript" src="../../../app/javascript/client.js"></script>
+    <script type="text/javascript" src="../../../app/javascript/testing/mockio.js"></script>
+
+    <!-- The module under test -->
+    <script type="text/javascript" src="../bug_tags_entry.js"></script>
+
+    <!-- The test suite -->
+    <script type="text/javascript" src="test_bug_tags_entry.js"></script>
+  </head>
+  <body class="yui3-skin-sam">
+  </body>
+</html>

=== added file 'lib/lp/bugs/javascript/tests/test_bug_tags_entry.js'
--- lib/lp/bugs/javascript/tests/test_bug_tags_entry.js	1970-01-01 00:00:00 +0000
+++ lib/lp/bugs/javascript/tests/test_bug_tags_entry.js	2012-01-05 19:12:40 +0000
@@ -0,0 +1,44 @@
+YUI().use('lp.testing.runner', 'lp.testing.mockio', 'test', 'console',
+          'lp.client', 'node-event-simulate', 'lp.bugs.bug_tags_entry',
+    function(Y) {
+
+    var module = Y.lp.bugs.bug_tags_entry;
+    var suite = new Y.Test.Suite("Bug tags entry Tests");
+
+    suite.add(new Y.Test.Case({
+        name: 'Tags parsing',
+
+        test_empty_string: function() {
+            var tag_string = '';
+            var results = module.parse_tags(tag_string);
+            Y.ArrayAssert.itemsAreEqual([], results);
+        },
+
+        test_one_item: function() {
+            var tag_string = 'cow';
+            var results = module.parse_tags(tag_string);
+            Y.ArrayAssert.itemsAreEqual(['cow'], results);
+        },
+
+        test_two_items: function() {
+            var tag_string = 'cow pig';
+            var results = module.parse_tags(tag_string);
+            Y.ArrayAssert.itemsAreEqual(['cow', 'pig'], results);
+        },
+
+        test_spaces: function() {
+            var tag_string = '   ';
+            var results = module.parse_tags(tag_string);
+            Y.ArrayAssert.itemsAreEqual([], results);
+        },
+
+        test_items_with_spaces: function() {
+            var tag_string = ' cow pig  chicken  ';
+            var results = module.parse_tags(tag_string);
+            Y.ArrayAssert.itemsAreEqual(['cow', 'pig', 'chicken'], results);
+        }
+
+      }));
+
+    Y.lp.testing.Runner.run(suite);
+});

=== modified file 'lib/lp/registry/browser/milestone.py'
--- lib/lp/registry/browser/milestone.py	2012-01-05 19:12:36 +0000
+++ lib/lp/registry/browser/milestone.py	2012-01-05 19:12:40 +0000
@@ -31,7 +31,10 @@
     implements,
     Interface,
     )
-from zope.schema import Choice, TextLine
+from zope.schema import (
+    Choice,
+    TextLine,
+    )
 
 from lp import _
 from lp.app.browser.launchpadform import (
@@ -428,14 +431,34 @@
 
     custom_widget('dateexpected', DateWidget)
 
+    def extendFields(self):
+        """See `LaunchpadFormView`.
+
+        Add a text-entry widget for milestone tags since there is not property
+        on the interface.
+        """
+        tag_entry = TextLine(
+            __name__='tags',
+            title=u'Tags',
+            required=False)
+        self.form_fields += form.Fields(
+            tag_entry, render_context=self.render_context)
+        # Make an instance attribute to avoid mutating the class attribute.
+        self.field_names = self.field_names[:]
+        # Insert the tags field before the summary.
+        self.field_names[3:3] = [tag_entry.__name__]
+
     @action(_('Register Milestone'), name='register')
     def register_action(self, action, data):
         """Use the newMilestone method on the context to make a milestone."""
-        self.context.newMilestone(
+        milestone = self.context.newMilestone(
             name=data.get('name'),
             code_name=data.get('code_name'),
             dateexpected=data.get('dateexpected'),
             summary=data.get('summary'))
+        tags = data.get('tags')
+        if tags:
+            milestone.setTags(tags.split(), self.user)
         self.next_url = canonical_url(self.context)
 
     @property

=== modified file 'lib/lp/registry/browser/tests/milestone-views.txt'
--- lib/lp/registry/browser/tests/milestone-views.txt	2011-12-30 08:13:14 +0000
+++ lib/lp/registry/browser/tests/milestone-views.txt	2012-01-05 19:12:40 +0000
@@ -508,12 +508,12 @@
 
     >>> owner = firefox.owner
     >>> login_person(owner)
-    >>> view = create_view(firefox_1_0, '+addmilestone')
+    >>> view = create_initialized_view(firefox_1_0, '+addmilestone')
     >>> print view.label
     Register a new milestone
 
     >>> view.field_names
-    ['name', 'code_name', 'dateexpected', 'summary']
+    ['name', 'code_name', 'dateexpected', 'tags', 'summary']
 
 The view provides an action_url and cancel_url properties that form
 submitting the form or aborting the action.

=== modified file 'lib/lp/registry/browser/tests/test_milestone.py'
--- lib/lp/registry/browser/tests/test_milestone.py	2012-01-05 19:12:36 +0000
+++ lib/lp/registry/browser/tests/test_milestone.py	2012-01-05 19:12:40 +0000
@@ -65,7 +65,7 @@
             }
         view = create_initialized_view(
             self.series, '+addmilestone', form=form)
-        # It's important to make sure no errors occured, but
+        # It's important to make sure no errors occured,
         # but also confirm that the milestone was created.
         self.assertEqual([], view.errors)
         self.assertEqual('1.1', self.product.milestones[0].name)
@@ -84,6 +84,19 @@
             "like YYYY-MM-DD format. The year must be after 1900.")
         self.assertEqual(expected_msg, error_msg)
 
+    def test_add_milestone_with_tags(self):
+        tags = u'zed alpha'
+        form = {
+            'field.name': '1.1',
+            'field.tags': tags,
+            'field.actions.register': 'Register Milestone',
+            }
+        view = create_initialized_view(
+            self.series, '+addmilestone', form=form)
+        self.assertEqual([], view.errors)
+        expected = sorted(tags.split())
+        self.assertEqual(expected, list(self.product.milestones[0].getTags()))
+
 
 class TestMilestoneMemcache(MemcacheTestCase):
 

=== modified file 'lib/lp/registry/interfaces/milestone.py'
--- lib/lp/registry/interfaces/milestone.py	2012-01-05 19:12:36 +0000
+++ lib/lp/registry/interfaces/milestone.py	2012-01-05 19:12:40 +0000
@@ -236,7 +236,6 @@
             description=_("Space-separated keywords for classifying "
                           "this milestone."),
             value_type=TextLine()))
-
     @call_with(user=REQUEST_USER)
     @export_write_operation()
     @operation_for_version('devel')

=== added file 'lib/lp/registry/javascript/__init__.py'
--- lib/lp/registry/javascript/__init__.py	1970-01-01 00:00:00 +0000
+++ lib/lp/registry/javascript/__init__.py	2012-01-05 19:12:40 +0000
@@ -0,0 +1,1 @@
+"""Tests for lp.registry.javascript.tests module."""

=== modified file 'lib/lp/registry/javascript/milestoneoverlay.js'
--- lib/lp/registry/javascript/milestoneoverlay.js	2011-08-09 14:18:02 +0000
+++ lib/lp/registry/javascript/milestoneoverlay.js	2012-01-05 19:12:40 +0000
@@ -1,4 +1,4 @@
-/* Copyright 2009 Canonical Ltd.  This software is licensed under the
+/* Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
  * GNU Affero General Public License version 3 (see the file LICENSE).
  *
  * A milestone form overlay that can create a milestone within any page.
@@ -14,6 +14,7 @@
     var milestone_form_uri;
     var series_uri;
     var next_step;
+    var client_sync = false;
 
     var save_new_milestone = function(data) {
 
@@ -33,14 +34,43 @@
             milestone_form.hide();
             // Reset the HTML form inside the widget.
             milestone_form.get('contentBox').one('form').reset();
-            next_step(parameters);
-        };
-
-        var client = new Y.lp.client.Launchpad();
+            if (Y.Lang.isValue(next_step)) {
+                next_step(parameters);
+            }
+        };
+
+        var set_milestone_tags = function(milestone) {
+            var tagstring = data['field.tags'][0].toLowerCase();
+            var tags = Y.lp.bugs.bug_tags_entry.parse_tags(tagstring);
+
+            if (tags.length === 0) {
+                return finish_new_milestone();
+            }
+
+            var parameters = {
+                tags: tags
+            };
+            milestone.named_post('setTags', {
+                parameters: parameters,
+                on: {
+                    success: finish_new_milestone,
+                    failure: function (ignore, response, args) {
+                        var error_box = Y.one('#milestone-error');
+                        var error_message = '<strong>' + response.statusText +
+                                            '</strong><p>' +
+                                            response.responseText +
+                                            '</p>';
+                        milestone_form.showError(error_message);
+                    }
+                }
+            });
+        };
+
+        var client = new Y.lp.client.Launchpad({sync: client_sync});
         client.named_post(series_uri, 'newMilestone', {
             parameters: parameters,
             on: {
-                success: finish_new_milestone,
+                success: set_milestone_tags,
                 failure: function (ignore, response, args) {
                     var error_box = Y.one('#milestone-error');
                     var error_message = '<strong>' + response.statusText +
@@ -52,7 +82,7 @@
             }
         });
     };
-
+    module.save_new_milestone = save_new_milestone;
 
     var setup_milestone_form = function () {
         var form_submit_button = Y.Node.create(
@@ -70,6 +100,7 @@
         Y.lp.app.calendar.add_calendar_widgets();
         milestone_form.show();
     };
+    module.setup_milestone_form = setup_milestone_form;
 
     var show_milestone_form = function(e) {
         e.preventDefault();
@@ -82,6 +113,29 @@
         }
     };
 
+    var configure = function(config) {
+        if (config === undefined) {
+            throw new Error(
+                "Missing attach_widget config for milestoneoverlay.");
+        }
+        if (config.milestone_form_uri === undefined ||
+            config.series_uri === undefined) {
+            throw new Error(
+                "attach_widget config for milestoneoverlay has " +
+                "undefined properties.");
+        }
+        milestone_form_uri = config.milestone_form_uri;
+        series_uri = config.series_uri;
+        // For testing purposes next_step may be undefined.
+        if (Y.Lang.isValue(config.next_step)) {
+            next_step = config.next_step;
+        }
+        if (Y.Lang.isValue(config.sync)) {
+            client_sync = config.sync;
+        }
+    };
+    module.configure = configure;
+
     /**
       * Attaches a milestone form overlay widget to an element.
       *
@@ -100,20 +154,7 @@
         if (Y.UA.ie) {
             return;
         }
-        if (config === undefined) {
-            throw new Error(
-                "Missing attach_widget config for milestoneoverlay.");
-        }
-        if (config.milestone_form_uri === undefined ||
-            config.series_uri === undefined ||
-            config.next_step === undefined) {
-            throw new Error(
-                "attach_widget config for milestoneoverlay has " +
-                "undefined properties.");
-        }
-        milestone_form_uri = config.milestone_form_uri;
-        series_uri = config.series_uri;
-        next_step = config.next_step;
+        configure(config);
         config.activate_node.on('click', show_milestone_form);
     };
 
@@ -123,5 +164,6 @@
                         "lp.anim",
                         "lazr.formoverlay",
                         "lp.app.calendar",
-                        "lp.client"
+                        "lp.client",
+                        "lp.bugs.bug_tags_entry"
                         ]});

=== added file 'lib/lp/registry/javascript/tests/__init__.py'
--- lib/lp/registry/javascript/tests/__init__.py	1970-01-01 00:00:00 +0000
+++ lib/lp/registry/javascript/tests/__init__.py	2012-01-05 19:12:40 +0000
@@ -0,0 +1,1 @@
+"""Tests for lp.registry.javascript.tests module."""

=== added file 'lib/lp/registry/javascript/tests/test_milestone_creation.js'
--- lib/lp/registry/javascript/tests/test_milestone_creation.js	1970-01-01 00:00:00 +0000
+++ lib/lp/registry/javascript/tests/test_milestone_creation.js	2012-01-05 19:12:40 +0000
@@ -0,0 +1,172 @@
+YUI({
+    base: '/+icing/yui/',
+    filter: 'raw', combine: false, fetchCSS: false
+}).use('test',
+       'lp.client',
+       'lp.testing.serverfixture',
+       'lp.registry.milestonetable',
+       'lp.registry.milestoneoverlay',
+       function(Y) {
+
+/**
+ * Integration tests for milestoneoverlay.
+ */
+var suite = new Y.Test.Suite("lp.registry.javascript.milestoneoverlay Tests");
+var serverfixture = Y.lp.testing.serverfixture;
+// Define the module-under-test.
+var the_module = Y.lp.registry.milestoneoverlay;
+
+var makeTestConfig = function(config) {
+  if (Y.Lang.isUndefined(config)) {
+    config = {};
+  }
+  config.on = Y.merge(
+    {
+      success: function(result) {
+          config.successful = true;
+          config.result = result;
+      },
+      failure: function(tid, response, args) {
+          config.successful = false;
+          config.result = {tid: tid, response: response, args: args};
+      }
+    },
+    config.on);
+  return config;
+};
+
+/**
+ * Test milestoneoverlay interaction via the API, such as creating
+ * milestones and adding tags.
+ */
+suite.add(new Y.Test.Case({
+    name: 'Milestone creation tests',
+
+    tearDown: function() {
+        // Always do this.
+        serverfixture.teardown(this);
+    },
+
+    test_configure: function() {
+        // Ensure configuring the module works as it will be needed in
+        // subsequent tests.
+        var config = {
+            milestone_form_uri: 'a',
+            series_uri: 'b',
+            next_step: 'c'
+        };
+        the_module.configure(config);
+    },
+
+    test_milestone_test_fixture_setup: function() {
+        // Setup the fixture, retrieving the objects we need for the test.
+        var data = serverfixture.setup(this, 'setup');
+        var client = new Y.lp.client.Launchpad({sync: true});
+        var config = makeTestConfig();
+        var product = new Y.lp.client.Entry(
+            client, data.product, data.product.self_link);
+        var product_name = product.get('name');
+        Y.Assert.areEqual('my-test-project', product_name);
+    },
+
+    test_milestone_creation_no_tags: function() {
+        // Setup the fixture, retrieving the objects we need for the test.
+        var data = serverfixture.setup(this, 'setup');
+
+        // Initialize the milestoneoverlay module.
+        var milestone_table = Y.lp.registry.milestonetable;
+        var config = {
+            milestone_form_uri: data.milestone_form_uri,
+            series_uri: data.series_uri,
+            //next_step: milestone_table.get_milestone_row,
+            sync: true
+        };
+        the_module.configure(config);
+        the_module.setup_milestone_form();
+
+        var milestone_name = 'new-milestone';
+        var code_name = 'new-codename';
+        var params = {
+            'field.name': [milestone_name],
+            'field.code_name': [code_name],
+            'field.dateexpected': [''],
+            'field.tags': [''],
+            'field.summary': ['']
+        };
+
+        // Test the creation of the new milestone.
+        the_module.save_new_milestone(params);
+
+        // Verify the milestone was created.
+        var client = new Y.lp.client.Launchpad({sync: true});
+        var product = new Y.lp.client.Entry(
+            client, data.product, data.product.self_link);
+        config = makeTestConfig({parameters: {name: milestone_name}});
+        // Get the new milestone.
+        product.named_get('getMilestone', config);
+        Y.Assert.isTrue(config.successful, 'Getting milestone failed');
+        var milestone = config.result;
+        Y.Assert.isInstanceOf(Y.lp.client.Entry, milestone);
+        Y.Assert.areEqual(milestone_name, milestone.get('name'));
+        Y.Assert.areEqual(code_name, milestone.get('code_name'));
+        // Ensure no tags are created.
+        config = makeTestConfig({parameters: {}});
+        milestone.named_get('getTags', config);
+        Y.Assert.isTrue(config.successful, 'call to getTags failed');
+        var expected = [];
+        Y.ArrayAssert.itemsAreEqual(expected, config.result);
+    },
+
+    test_milestone_creation_with_tags: function() {
+        // Setup the fixture, retrieving the objects we need for the test.
+        var data = serverfixture.setup(this, 'setup');
+
+        // Initialize the milestoneoverlay module.
+        var milestone_table = Y.lp.registry.milestonetable;
+        var config = {
+            milestone_form_uri: data.milestone_form_uri,
+            series_uri: data.series_uri,
+            //next_step: milestone_table.get_milestone_row,
+            sync: true
+        };
+        the_module.configure(config);
+        the_module.setup_milestone_form();
+
+        var milestone_name = 'new-milestone';
+        var code_name = 'new-codename';
+        var tags = ['zeta  alpha beta'];
+        var params = {
+            'field.name': [milestone_name],
+            'field.code_name': [code_name],
+            'field.dateexpected': [''],
+            'field.tags': tags,
+            'field.summary': ['']
+        };
+
+        // Test the creation of the new milestone.
+        the_module.save_new_milestone(params);
+
+        // Verify the milestone was created.
+        var client = new Y.lp.client.Launchpad({sync: true});
+        var product = new Y.lp.client.Entry(
+            client, data.product, data.product.self_link);
+        config = makeTestConfig({parameters: {name: milestone_name}});
+        // Get the new milestone.
+        product.named_get('getMilestone', config);
+        Y.Assert.isTrue(config.successful, 'Getting milestone failed');
+        var milestone = config.result;
+        Y.Assert.isInstanceOf(Y.lp.client.Entry, milestone);
+        Y.Assert.areEqual(milestone_name, milestone.get('name'));
+        Y.Assert.areEqual(code_name, milestone.get('code_name'));
+        // Ensure the tags are created.
+        config = makeTestConfig({parameters: {}});
+        milestone.named_get('getTags', config);
+        Y.Assert.isTrue(config.successful, 'call to getTags failed');
+        var expected = ["alpha", "beta", "zeta"];
+        Y.ArrayAssert.itemsAreEqual(expected, config.result);
+    }
+}));
+
+// The last line is necessary.  Include it.
+serverfixture.run(suite);
+});

=== added file 'lib/lp/registry/javascript/tests/test_milestone_creation.py'
--- lib/lp/registry/javascript/tests/test_milestone_creation.py	1970-01-01 00:00:00 +0000
+++ lib/lp/registry/javascript/tests/test_milestone_creation.py	2012-01-05 19:12:40 +0000
@@ -0,0 +1,39 @@
+# Copyright 2011-2012 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Support for the lp.registry.javascript.milestoneoverlay YUIXHR tests.
+"""
+
+__metaclass__ = type
+__all__ = []
+
+from lp.services.webapp.publisher import canonical_url
+from lp.testing import person_logged_in
+from lp.testing.factory import LaunchpadObjectFactory
+from lp.testing.yuixhr import (
+    login_as_person,
+    make_suite,
+    setup,
+    )
+
+
+factory = LaunchpadObjectFactory()
+
+
+@setup
+def setup(request, data):
+    owner = factory.makePerson()
+    with person_logged_in(owner):
+        product = factory.makeProduct(name="my-test-project", owner=owner)
+        product_series = factory.makeProductSeries(
+            name="new-series", product=product)
+        data['product'] = product
+        data['series_uri'] = canonical_url(
+            product_series, path_only_if_possible=True)
+        data['milestone_form_uri'] = (
+            canonical_url(product_series) + '/+addmilestone/++form++')
+    login_as_person(owner)
+
+
+def test_suite():
+    return make_suite(__name__)

=== modified file 'lib/lp/registry/model/distroseries.py'
--- lib/lp/registry/model/distroseries.py	2012-01-03 05:05:39 +0000
+++ lib/lp/registry/model/distroseries.py	2012-01-05 19:12:40 +0000
@@ -1408,12 +1408,15 @@
         return distroarchseries
 
     def newMilestone(self, name, dateexpected=None, summary=None,
-                     code_name=None):
+                     code_name=None, tags=None):
         """See `IDistroSeries`."""
-        return Milestone(
+        milestone = Milestone(
             name=name, code_name=code_name,
             dateexpected=dateexpected, summary=summary,
             distribution=self.distribution, distroseries=self)
+        if tags:
+            milestone.setTags(tags.split())
+        return milestone
 
     def getLatestUploads(self):
         """See `IDistroSeries`."""

=== modified file 'lib/lp/registry/model/milestone.py'
--- lib/lp/registry/model/milestone.py	2012-01-05 19:12:36 +0000
+++ lib/lp/registry/model/milestone.py	2012-01-05 19:12:40 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2012 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 # pylint: disable-msg=E0611,W0212

=== modified file 'lib/lp/registry/model/product.py'
--- lib/lp/registry/model/product.py	2011-12-30 06:14:56 +0000
+++ lib/lp/registry/model/product.py	2012-01-05 19:12:40 +0000
@@ -926,10 +926,11 @@
 
     def getMilestone(self, name):
         """See `IProduct`."""
-        return Milestone.selectOne("""
+        results = Milestone.selectOne("""
             product = %s AND
             name = %s
             """ % sqlvalues(self.id, name))
+        return results
 
     def createBug(self, bug_params):
         """See `IBugTarget`."""

=== modified file 'lib/lp/registry/model/productseries.py'
--- lib/lp/registry/model/productseries.py	2011-12-30 06:14:56 +0000
+++ lib/lp/registry/model/productseries.py	2012-01-05 19:12:40 +0000
@@ -541,11 +541,14 @@
         return history
 
     def newMilestone(self, name, dateexpected=None, summary=None,
-                     code_name=None):
+                     code_name=None, tags=None):
         """See IProductSeries."""
-        return Milestone(
+        milestone = Milestone(
             name=name, dateexpected=dateexpected, summary=summary,
             product=self.product, productseries=self, code_name=code_name)
+        if tags:
+            milestone.setTags(tags.split())
+        return milestone
 
     def getTemplatesCollection(self):
         """See `IHasTranslationTemplates`."""