← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~matsubara/launchpad/ec2-land-update-mp into lp:launchpad

 

Diogo Matsubara has proposed merging lp:~matsubara/launchpad/ec2-land-update-mp into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)


Hi there,

this branch changes the ec2 land command to add the built commit message to the merge proposal.
It also changes the method to get_commit_message to build_commit_message to make it more clear what it's doing.


To run tests: bin/test -tt test_autoland
-- 
https://code.launchpad.net/~matsubara/launchpad/ec2-land-update-mp/+merge/40228
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~matsubara/launchpad/ec2-land-update-mp into lp:launchpad.
=== modified file 'lib/canonical/launchpad/interfaces/_schema_circular_imports.py'
--- lib/canonical/launchpad/interfaces/_schema_circular_imports.py	2010-11-03 22:37:55 +0000
+++ lib/canonical/launchpad/interfaces/_schema_circular_imports.py	2010-11-05 19:52:03 +0000
@@ -383,7 +383,7 @@
     IDistroSeries, 'getPackageUploads', IPackageUpload)
 patch_reference_property(IDistroSeries, 'parent_series', IDistroSeries)
 patch_plain_parameter_type(
-    IDistroSeries, 'deriveDistroSeries', 'distribution', IDistroSeries)
+    IDistroSeries, 'deriveDistroSeries', 'distribution', IDistribution)
 
 # IDistroSeriesDifferenceComment
 IDistroSeriesDifferenceComment['comment_author'].schema = IPerson

=== modified file 'lib/devscripts/autoland.py'
--- lib/devscripts/autoland.py	2010-10-24 21:00:11 +0000
+++ lib/devscripts/autoland.py	2010-11-05 19:52:03 +0000
@@ -160,21 +160,35 @@
         # URL. Do it ourselves.
         return URI(scheme='bzr+ssh', host=host, path='/' + branch.unique_name)
 
-    def get_commit_message(self, commit_text, testfix=False, no_qa=False,
+    def build_commit_message(self, commit_text, testfix=False, no_qa=False,
                            incremental=False, rollback=None):
         """Get the Launchpad-style commit message for a merge proposal."""
         reviews = self.get_reviews()
         bugs = self.get_bugs()
 
-        tags = ''.join([
+        tags = [
             get_testfix_clause(testfix),
             get_reviewer_clause(reviews),
             get_bugs_clause(bugs),
             get_qa_clause(bugs, no_qa,
                 incremental, rollback=rollback),
-            ])
-
-        return '%s %s' % (tags, commit_text)
+            ]
+
+        # Make sure we don't add duplicated tags to commit_text.
+        commit_tags = tags[:]
+        for tag in tags:
+            if tag in commit_text:
+                commit_tags.remove(tag)
+
+        if commit_tags:
+            return '%s %s' % (''.join(commit_tags), commit_text)
+        else:
+            return commit_text
+
+    def set_commit_message(self, commit_message):
+        """Set the Launchpad-style commit message for a merge proposal."""
+        self._mp.commit_message = commit_message
+        self._mp.lp_save()
 
 
 def get_testfix_clause(testfix=False):

=== modified file 'lib/devscripts/ec2test/builtins.py'
--- lib/devscripts/ec2test/builtins.py	2010-10-29 14:52:10 +0000
+++ lib/devscripts/ec2test/builtins.py	2010-11-05 19:52:03 +0000
@@ -411,7 +411,7 @@
         if rollback and (no_qa or incremental):
             print "--rollback option used. Ignoring --no-qa and --incremental."
         try:
-            commit_message = mp.get_commit_message(
+            commit_message = mp.build_commit_message(
                 commit_text, testfix, no_qa, incremental, rollback=rollback)
         except MissingReviewError:
             raise BzrCommandError(
@@ -428,6 +428,15 @@
                 "--incremental option requires bugs linked to the branch. "
                 "Link the bugs or remove the --incremental option.")
 
+        # Override the commit message in the MP with the commit message built
+        # with the proper tags.
+        try:
+            mp.set_commit_message(commit_message)
+        except Exception, e:
+            raise BzrCommandError(
+                "Unable to set the commit message in the merge proposal.\n"
+                "Got: %s" % e)
+
         if print_commit:
             print commit_message
             return

=== modified file 'lib/devscripts/tests/test_autoland.py'
--- lib/devscripts/tests/test_autoland.py	2010-09-03 15:13:01 +0000
+++ lib/devscripts/tests/test_autoland.py	2010-11-05 19:52:03 +0000
@@ -59,6 +59,10 @@
 
     def __init__(self, root=None):
         self._root = root
+        self.commit_message = None
+
+    def lp_save(self):
+        pass
 
 
 class TestPQMRegexAcceptance(unittest.TestCase):
@@ -91,7 +95,7 @@
             raise self.failureException(msg)
 
     def _test_commit_message_match(self, incr, no_qa, testfix):
-        commit_message = self.mp.get_commit_message("Foobaring the sbrubble.",
+        commit_message = self.mp.build_commit_message("Foobaring the sbrubble.",
             testfix, no_qa, incr)
         self.assertRegexpMatches(commit_message, self.devel_open_re)
         self.assertRegexpMatches(commit_message, self.dbdevel_normal_re)
@@ -150,7 +154,7 @@
         self.mp.get_reviews = FakeMethod({None : [self.fake_person]})
 
         self.assertEqual("[r=foo][ui=none][bug=20] Foobaring the sbrubble.",
-            self.mp.get_commit_message("Foobaring the sbrubble.",
+            self.mp.build_commit_message("Foobaring the sbrubble.",
                 testfix, no_qa, incr))
 
     def test_commit_no_bugs_no_noqa(self):
@@ -161,7 +165,7 @@
         self.mp.get_bugs = FakeMethod([])
         self.mp.get_reviews = FakeMethod({None : [self.fake_person]})
 
-        self.assertRaises(MissingBugsError, self.mp.get_commit_message,
+        self.assertRaises(MissingBugsError, self.mp.build_commit_message,
             testfix, no_qa, incr)
 
     def test_commit_no_bugs_with_noqa(self):
@@ -173,7 +177,7 @@
         self.mp.get_reviews = FakeMethod({None : [self.fake_person]})
 
         self.assertEqual("[r=foo][ui=none][no-qa] Foobaring the sbrubble.",
-            self.mp.get_commit_message("Foobaring the sbrubble.",
+            self.mp.build_commit_message("Foobaring the sbrubble.",
                 testfix, no_qa, incr))
 
     def test_commit_bugs_with_noqa(self):
@@ -186,7 +190,7 @@
 
         self.assertEqual(
             "[r=foo][ui=none][bug=20][no-qa] Foobaring the sbrubble.",
-            self.mp.get_commit_message("Foobaring the sbrubble.",
+            self.mp.build_commit_message("Foobaring the sbrubble.",
                 testfix, no_qa, incr))
 
     def test_commit_bugs_with_incr(self):
@@ -199,7 +203,7 @@
 
         self.assertEqual(
             "[r=foo][ui=none][bug=20][incr] Foobaring the sbrubble.",
-            self.mp.get_commit_message("Foobaring the sbrubble.",
+            self.mp.build_commit_message("Foobaring the sbrubble.",
                 testfix, no_qa, incr))
 
     def test_commit_no_bugs_with_incr(self):
@@ -212,7 +216,7 @@
 
         self.assertEqual(
             "[r=foo][ui=none][bug=20][incr] Foobaring the sbrubble.",
-            self.mp.get_commit_message("Foobaring the sbrubble.",
+            self.mp.build_commit_message("Foobaring the sbrubble.",
                 testfix, no_qa, incr))
 
     def test_commit_with_noqa_and_incr(self):
@@ -225,7 +229,7 @@
 
         self.assertEqual(
             "[r=foo][ui=none][bug=20][no-qa][incr] Foobaring the sbrubble.",
-            self.mp.get_commit_message("Foobaring the sbrubble.", 
+            self.mp.build_commit_message("Foobaring the sbrubble.", 
                 testfix, no_qa, incr))
 
     def test_commit_with_rollback(self):
@@ -234,8 +238,29 @@
 
         self.assertEqual(
             "[r=foo][ui=none][bug=20][rollback=123] Foobaring the sbrubble.",
-            self.mp.get_commit_message("Foobaring the sbrubble.", 
-                rollback=123))
+            self.mp.build_commit_message("Foobaring the sbrubble.", 
+                rollback=123))
+
+    def test_takes_into_account_existing_tags_on_commit_text(self):
+        self.mp.get_bugs = FakeMethod([self.fake_bug])
+        self.mp.get_reviews = FakeMethod({None : [self.fake_person]})
+
+        self.assertEqual(
+            "[r=foo][ui=none][bug=20][rollback=123] Foobaring the sbrubble.",
+            self.mp.build_commit_message(
+                "[r=foo][ui=none][bug=20][rollback=123] Foobaring the sbrubble.",
+                rollback=123))
+
+
+class TestSetCommitMessage(unittest.TestCase):
+
+    def setUp(self):
+        self.mp = MergeProposal(FakeLPMergeProposal())
+
+    def test_set_commit_message(self):
+        commit_message = "Foobaring the sbrubble."
+        self.mp.set_commit_message(commit_message)
+        self.assertEqual(self.mp._mp.commit_message, commit_message)
 
 
 class TestGetTestfixClause(unittest.TestCase):

=== modified file 'lib/lp/blueprints/browser/configure.zcml'
--- lib/lp/blueprints/browser/configure.zcml	2010-10-03 15:30:06 +0000
+++ lib/lp/blueprints/browser/configure.zcml	2010-11-05 19:52:03 +0000
@@ -8,6 +8,21 @@
     xmlns:i18n="http://namespaces.zope.org/i18n";
     xmlns:xmlrpc="http://namespaces.zope.org/xmlrpc";
     i18n_domain="launchpad">
+
+  <adapter
+      name="blueprints"
+      provides="canonical.launchpad.webapp.interfaces.IBreadcrumb"
+      for="lp.blueprints.interfaces.specificationtarget.IHasSpecifications"
+      factory="lp.blueprints.browser.specificationtarget.BlueprintsVHostBreadcrumb"
+      permission="zope.Public"/>
+  <adapter
+      name="blueprints"
+      provides="canonical.launchpad.webapp.interfaces.IBreadcrumb"
+      for="lp.registry.interfaces.person.IPerson"
+      factory="lp.blueprints.browser.specificationtarget.BlueprintsVHostBreadcrumb"
+      permission="zope.Public"/>
+
+
     <browser:navigation
         module="lp.blueprints.browser.sprint"
         classes="

=== modified file 'lib/lp/blueprints/browser/specification.py'
--- lib/lp/blueprints/browser/specification.py	2010-11-01 03:32:29 +0000
+++ lib/lp/blueprints/browser/specification.py	2010-11-05 19:52:03 +0000
@@ -536,13 +536,14 @@
 
     @action(_('Change'), name='change')
     def change_action(self, action, data):
+        old_status = self.context.lifecycle_status
         self.updateContextFromData(data)
         # We need to ensure that resolution is recorded if the spec is now
         # resolved.
-        newstate = self.context.updateLifecycleStatus(self.user)
-        if newstate is not None:
+        new_status = self.context.lifecycle_status
+        if new_status != old_status:
             self.request.response.addNotification(
-                'blueprint is now considered "%s".' % newstate.title)
+                'Blueprint is now considered "%s".' % new_status.title)
         self.next_url = canonical_url(self.context)
 
 
@@ -735,7 +736,7 @@
 class SpecificationSupersedingView(LaunchpadFormView):
     schema = ISpecification
     field_names = ['superseded_by']
-    label = _('Mark specification superseded')
+    label = _('Mark blueprint superseded')
     custom_widget('superseded_by', SupersededByWidget)
 
     @property
@@ -762,7 +763,7 @@
                 description=_(
                     "The blueprint which supersedes this one. Note "
                     "that selecting a blueprint here and pressing "
-                    "Continue will change the specification status "
+                    "Continue will change the blueprint status "
                     "to Superseded.")),
             render_context=self.render_context)
 
@@ -784,7 +785,7 @@
         newstate = self.context.updateLifecycleStatus(self.user)
         if newstate is not None:
             self.request.response.addNotification(
-                'Specification is now considered "%s".' % newstate.title)
+                'Blueprint is now considered "%s".' % newstate.title)
         self.next_url = canonical_url(self.context)
 
     @property

=== modified file 'lib/lp/blueprints/browser/tests/test_specification.py'
--- lib/lp/blueprints/browser/tests/test_specification.py	2010-08-20 20:31:18 +0000
+++ lib/lp/blueprints/browser/tests/test_specification.py	2010-11-05 19:52:03 +0000
@@ -8,10 +8,13 @@
 from lazr.restful.testing.webservice import FakeRequest
 from zope.publisher.interfaces import NotFound
 
+from canonical.launchpad.webapp.interfaces import BrowserNotificationLevel
 from canonical.launchpad.webapp.servers import StepsToGo
 from canonical.testing.layers import DatabaseFunctionalLayer
 from lp.blueprints.browser import specification
-from lp.testing import TestCaseWithFactory
+from lp.blueprints.enums import SpecificationImplementationStatus
+from lp.testing import login_person, TestCaseWithFactory
+from lp.testing.views import create_initialized_view
 
 
 class LocalFakeRequest(FakeRequest):
@@ -108,6 +111,82 @@
             self.specification.getBranchLink(branch), self.traverse(segments))
 
 
+class TestSpecificationEditStatusView(TestCaseWithFactory):
+    """Test the SpecificationEditStatusView."""
+
+    layer = DatabaseFunctionalLayer
+
+    def test_records_started(self):
+        spec = self.factory.makeSpecification(
+            implementation_status=SpecificationImplementationStatus.NOTSTARTED)
+        login_person(spec.owner)
+        form = {
+            'field.implementation_status': 'STARTED',
+            'field.actions.change': 'Change',
+            }
+        view = create_initialized_view(spec, name='+status', form=form)
+        self.assertEqual(
+            SpecificationImplementationStatus.STARTED, spec.implementation_status)
+        self.assertEqual(spec.owner, spec.starter)
+        [notification] = view.request.notifications
+        self.assertEqual(BrowserNotificationLevel.NOTICE, notification.level)
+        self.assertEqual(
+            'Blueprint is now considered "Started".', notification.message)
+
+    def test_unchanged_lifecycle_has_no_notification(self):
+        spec = self.factory.makeSpecification(
+            implementation_status=SpecificationImplementationStatus.STARTED)
+        login_person(spec.owner)
+        form = {
+            'field.implementation_status': 'SLOW',
+            'field.actions.change': 'Change',
+            }
+        view = create_initialized_view(spec, name='+status', form=form)
+        self.assertEqual(
+            SpecificationImplementationStatus.SLOW, spec.implementation_status)
+        self.assertEqual(0, len(view.request.notifications))
+
+    def test_records_unstarting(self):
+        # If a spec was started, and is changed to not started, a notice is shown.
+        # Also the spec.starter is cleared out.
+        spec = self.factory.makeSpecification(
+            implementation_status=SpecificationImplementationStatus.STARTED)
+        login_person(spec.owner)
+        form = {
+            'field.implementation_status': 'NOTSTARTED',
+            'field.actions.change': 'Change',
+            }
+        view = create_initialized_view(spec, name='+status', form=form)
+        self.assertEqual(
+            SpecificationImplementationStatus.NOTSTARTED,
+            spec.implementation_status)
+        self.assertIs(None, spec.starter)
+        [notification] = view.request.notifications
+        self.assertEqual(BrowserNotificationLevel.NOTICE, notification.level)
+        self.assertEqual(
+            'Blueprint is now considered "Not started".', notification.message)
+
+    def test_records_completion(self):
+        # If a spec is marked as implemented the user is notifiec it is now
+        # complete.
+        spec = self.factory.makeSpecification(
+            implementation_status=SpecificationImplementationStatus.STARTED)
+        login_person(spec.owner)
+        form = {
+            'field.implementation_status': 'IMPLEMENTED',
+            'field.actions.change': 'Change',
+            }
+        view = create_initialized_view(spec, name='+status', form=form)
+        self.assertEqual(
+            SpecificationImplementationStatus.IMPLEMENTED,
+            spec.implementation_status)
+        self.assertEqual(spec.owner, spec.completer)
+        [notification] = view.request.notifications
+        self.assertEqual(BrowserNotificationLevel.NOTICE, notification.level)
+        self.assertEqual(
+            'Blueprint is now considered "Complete".', notification.message)
+
+
 class TestSecificationHelpers(unittest.TestCase):
     """Test specification helper functions."""
 

=== modified file 'lib/lp/blueprints/configure.zcml'
--- lib/lp/blueprints/configure.zcml	2010-08-25 04:12:59 +0000
+++ lib/lp/blueprints/configure.zcml	2010-11-05 19:52:03 +0000
@@ -22,258 +22,220 @@
       name="blueprints" />
 
 
-    <lp:help-folder
-        folder="help" type="lp.blueprints.publisher.BlueprintsLayer" />
-
-    <!-- Sprint -->
-
-    <class
-        class="lp.blueprints.model.sprint.Sprint">
-        <allow
-            interface="lp.blueprints.interfaces.sprint.ISprint"/>
-        <require
-            permission="launchpad.AnyPerson"
-            set_schema="lp.blueprints.interfaces.sprint.ISprint"/>
-    </class>
-    <adapter
-        provides="canonical.launchpad.webapp.interfaces.IBreadcrumb"
-        for="lp.blueprints.interfaces.sprint.ISprint"
-        factory="canonical.launchpad.webapp.breadcrumb.TitleBreadcrumb"
-        permission="zope.Public"/>
-    <adapter
-        provides="canonical.launchpad.webapp.interfaces.IBreadcrumb"
-        for="lp.blueprints.interfaces.sprint.ISprintSet"
-        factory="lp.blueprints.browser.sprint.SprintSetBreadcrumb"
-        permission="zope.Public"/>
-    <adapter
-        provides="canonical.launchpad.webapp.interfaces.IBreadcrumb"
-        for="lp.blueprints.interfaces.specification.ISpecification"
-        factory="canonical.launchpad.webapp.breadcrumb.TitleBreadcrumb"
-        permission="zope.Public"/>
-
-    <!-- This is a view used to export data needed by the sprint scheduler.
-                         As there are no API stability guarantees, the view name starts
-                         with "temp" to discourage people from relying on it. -->
-
-
-    <!-- SprintSet -->
-
-    <class
-        class="lp.blueprints.model.sprint.SprintSet">
-        <allow
-            interface="lp.blueprints.interfaces.sprint.ISprintSet"/>
-    </class>
-    <securedutility
-        class="lp.blueprints.model.sprint.SprintSet"
-        provides="lp.blueprints.interfaces.sprint.ISprintSet">
-        <allow
-            interface="lp.blueprints.interfaces.sprint.ISprintSet"/>
-    </securedutility>
-
-    <!-- SprintSpecification -->
-
-    <class
-        class="lp.blueprints.model.sprintspecification.SprintSpecification">
-        <allow
-            interface="lp.blueprints.interfaces.sprintspecification.ISprintSpecification"/>
-        <require
-            permission="launchpad.Edit"
-            set_attributes="whiteboard"/>
-    </class>
-
-    <!-- SpecificationDependency -->
-
-    <class
-        class="lp.blueprints.model.specificationdependency.SpecificationDependency">
-        <allow
-            interface="lp.blueprints.interfaces.specificationdependency.ISpecificationDependency"/>
-        <require
-            permission="zope.Public"
-            set_schema="lp.blueprints.interfaces.specificationdependency.ISpecificationDependency"/>
-    </class>
-    <adapter
-        for="lp.blueprints.interfaces.specificationdependency.ISpecificationDependency"
-        factory="lp.blueprints.interfaces.specificationdependency.SpecDependencyIsAlsoRemoval"
-        provides="lp.blueprints.interfaces.specificationdependency.ISpecificationDependencyRemoval"/>
-
-    <!-- SpecificationSubscription -->
-
-    <class
-        class="lp.blueprints.model.specificationsubscription.SpecificationSubscription">
-        <allow
-            interface="lp.blueprints.interfaces.specificationsubscription.ISpecificationSubscription"/>
-        <require
-            permission="launchpad.Edit"
-            set_attributes="essential"/>
-    </class>
-    <subscriber
-        for="lp.blueprints.interfaces.specificationsubscription.ISpecificationSubscription                                                lazr.lifecycle.interfaces.IObjectCreatedEvent"
-        handler="canonical.launchpad.mailnotification.notify_specification_subscription_created"/>
-    <subscriber
-        for="lp.blueprints.interfaces.specificationsubscription.ISpecificationSubscription                                                lazr.lifecycle.interfaces.IObjectModifiedEvent"
-        handler="canonical.launchpad.mailnotification.notify_specification_subscription_modified"/>
-
-    <!-- SpecificationFeedback -->
-
-    <class
-        class="lp.blueprints.model.specificationfeedback.SpecificationFeedback">
-        <allow
-            interface="lp.blueprints.interfaces.specificationfeedback.ISpecificationFeedback"/>
-        <require
-            permission="zope.Public"
-            set_schema="lp.blueprints.interfaces.specificationfeedback.ISpecificationFeedback"/>
-    </class>
-
-    <!-- SprintAttendance -->
-
-    <class
-        class="lp.blueprints.model.sprintattendance.SprintAttendance">
-        <allow
-            interface="lp.blueprints.interfaces.sprintattendance.ISprintAttendance"/>
-        <require
-            permission="launchpad.Edit"
-            set_attributes="time_starts time_ends"/>
-    </class>
-
-    <!-- ISpecificationBranch -->
-
-    <class
-        class="lp.blueprints.model.specificationbranch.SpecificationBranch">
-        <allow
-            interface="lp.blueprints.interfaces.specificationbranch.ISpecificationBranch"/>
-        <require
-            permission="launchpad.AnyPerson"
-            set_attributes="summary"/>
-    </class>
-    <subscriber
-        for="lp.blueprints.interfaces.specificationbranch.ISpecificationBranch                                                lazr.lifecycle.interfaces.IObjectCreatedEvent"
-        handler="canonical.launchpad.subscribers.karma.spec_branch_created"/>
-    <facet
-        facet="specifications"/>
-
-    <!-- SpecificationBranchSet -->
-
-    <securedutility
-        class="lp.blueprints.model.specificationbranch.SpecificationBranchSet"
-        provides="lp.blueprints.interfaces.specificationbranch.ISpecificationBranchSet">
-        <allow
-            interface="lp.blueprints.interfaces.specificationbranch.ISpecificationBranchSet"/>
-    </securedutility>
-    <facet
-        facet="specifications">
-
-        <!-- Specification -->
-
-        <class
-            class="lp.blueprints.model.specification.Specification">
-            <allow
-                interface="lp.blueprints.interfaces.specification.ISpecification"/>
-
-            <!-- We allow any authenticated person to update the whiteboard -->
-
-            <require
-                permission="launchpad.AnyPerson"
-                set_attributes="whiteboard"/>
-
-            <!-- NB: goals and goalstatus are not to be set directly, it should
-                                 only be set through the proposeGoal / acceptBy / declineBy
-                                 methods
-                                 -->
-
-            <require
-                permission="launchpad.Edit"
-                set_attributes="name title summary definition_status specurl                                                                                         superseded_by milestone                                                                                         product distribution approver assignee drafter                                                                                         man_days implementation_status"/>
-            <require
-                permission="launchpad.Admin"
-                set_attributes="priority direction_approved"/>
-            <allow
-                attributes="
-                    bugs
+  <lp:help-folder
+      folder="help" type="lp.blueprints.publisher.BlueprintsLayer" />
+
+  <!-- Sprint -->
+
+  <class
+      class="lp.blueprints.model.sprint.Sprint">
+    <allow
+        interface="lp.blueprints.interfaces.sprint.ISprint"/>
+    <require
+        permission="launchpad.AnyPerson"
+        set_schema="lp.blueprints.interfaces.sprint.ISprint"/>
+  </class>
+  <adapter
+      provides="canonical.launchpad.webapp.interfaces.IBreadcrumb"
+      for="lp.blueprints.interfaces.sprint.ISprint"
+      factory="canonical.launchpad.webapp.breadcrumb.TitleBreadcrumb"
+      permission="zope.Public"/>
+  <adapter
+      provides="canonical.launchpad.webapp.interfaces.IBreadcrumb"
+      for="lp.blueprints.interfaces.sprint.ISprintSet"
+      factory="lp.blueprints.browser.sprint.SprintSetBreadcrumb"
+      permission="zope.Public"/>
+  <adapter
+      provides="canonical.launchpad.webapp.interfaces.IBreadcrumb"
+      for="lp.blueprints.interfaces.specification.ISpecification"
+      factory="canonical.launchpad.webapp.breadcrumb.TitleBreadcrumb"
+      permission="zope.Public"/>
+
+  <!-- This is a view used to export data needed by the sprint scheduler.
+       As there are no API stability guarantees, the view name starts
+       with "temp" to discourage people from relying on it. -->
+
+  <!-- SprintSet -->
+
+  <class
+      class="lp.blueprints.model.sprint.SprintSet">
+    <allow interface="lp.blueprints.interfaces.sprint.ISprintSet"/>
+  </class>
+  <securedutility
+      class="lp.blueprints.model.sprint.SprintSet"
+      provides="lp.blueprints.interfaces.sprint.ISprintSet">
+    <allow interface="lp.blueprints.interfaces.sprint.ISprintSet"/>
+  </securedutility>
+
+  <!-- SprintSpecification -->
+
+  <class
+      class="lp.blueprints.model.sprintspecification.SprintSpecification">
+    <allow
+        interface="lp.blueprints.interfaces.sprintspecification.ISprintSpecification"/>
+    <require
+        permission="launchpad.Edit"
+        set_attributes="whiteboard"/>
+  </class>
+
+  <!-- SpecificationDependency -->
+
+  <class class="lp.blueprints.model.specificationdependency.SpecificationDependency">
+    <allow interface="lp.blueprints.interfaces.specificationdependency.ISpecificationDependency"/>
+    <require
+        permission="zope.Public"
+        set_schema="lp.blueprints.interfaces.specificationdependency.ISpecificationDependency"/>
+  </class>
+  <adapter
+      for="lp.blueprints.interfaces.specificationdependency.ISpecificationDependency"
+      factory="lp.blueprints.interfaces.specificationdependency.SpecDependencyIsAlsoRemoval"
+      provides="lp.blueprints.interfaces.specificationdependency.ISpecificationDependencyRemoval"/>
+
+  <!-- SpecificationSubscription -->
+
+  <class class="lp.blueprints.model.specificationsubscription.SpecificationSubscription">
+    <allow
+        interface="lp.blueprints.interfaces.specificationsubscription.ISpecificationSubscription"/>
+    <require
+        permission="launchpad.Edit"
+        set_attributes="essential"/>
+  </class>
+  <subscriber
+      for="lp.blueprints.interfaces.specificationsubscription.ISpecificationSubscription
+           lazr.lifecycle.interfaces.IObjectCreatedEvent"
+      handler="canonical.launchpad.mailnotification.notify_specification_subscription_created"/>
+  <subscriber
+      for="lp.blueprints.interfaces.specificationsubscription.ISpecificationSubscription
+           lazr.lifecycle.interfaces.IObjectModifiedEvent"
+      handler="canonical.launchpad.mailnotification.notify_specification_subscription_modified"/>
+
+  <!-- SpecificationFeedback -->
+
+  <class class="lp.blueprints.model.specificationfeedback.SpecificationFeedback">
+    <allow interface="lp.blueprints.interfaces.specificationfeedback.ISpecificationFeedback"/>
+    <require
+        permission="zope.Public"
+        set_schema="lp.blueprints.interfaces.specificationfeedback.ISpecificationFeedback"/>
+  </class>
+
+  <!-- SprintAttendance -->
+
+  <class class="lp.blueprints.model.sprintattendance.SprintAttendance">
+    <allow interface="lp.blueprints.interfaces.sprintattendance.ISprintAttendance"/>
+    <require
+        permission="launchpad.Edit"
+        set_attributes="time_starts time_ends"/>
+  </class>
+
+  <!-- ISpecificationBranch -->
+
+  <class class="lp.blueprints.model.specificationbranch.SpecificationBranch">
+    <allow interface="lp.blueprints.interfaces.specificationbranch.ISpecificationBranch"/>
+    <require
+        permission="launchpad.AnyPerson"
+        set_attributes="summary"/>
+  </class>
+  <subscriber
+      for="lp.blueprints.interfaces.specificationbranch.ISpecificationBranch
+           lazr.lifecycle.interfaces.IObjectCreatedEvent"
+      handler="canonical.launchpad.subscribers.karma.spec_branch_created"/>
+
+  <!-- SpecificationBranchSet -->
+
+  <securedutility
+      class="lp.blueprints.model.specificationbranch.SpecificationBranchSet"
+      provides="lp.blueprints.interfaces.specificationbranch.ISpecificationBranchSet">
+    <allow interface="lp.blueprints.interfaces.specificationbranch.ISpecificationBranchSet"/>
+  </securedutility>
+
+  <!-- Specification -->
+
+  <class class="lp.blueprints.model.specification.Specification">
+    <allow interface="lp.blueprints.interfaces.specification.ISpecification"/>
+    <!-- We allow any authenticated person to update the whiteboard -->
+    <require
+        permission="launchpad.AnyPerson"
+        set_attributes="whiteboard"/>
+    <!-- NB: goals and goalstatus are not to be set directly, it should
+         only be set through the proposeGoal / acceptBy / declineBy
+         methods -->
+    <require
+        permission="launchpad.Edit"
+        set_attributes="name title summary definition_status specurl
+                        superseded_by milestone product distribution
+                        approver assignee drafter man_days
+                        implementation_status"/>
+    <require
+        permission="launchpad.Admin"
+        set_attributes="priority direction_approved"/>
+    <allow
+        attributes="bugs
                     bug_links"/>
-            <require
-                permission="launchpad.AnyPerson"
-                attributes="
-                    linkBug
+    <require
+        permission="launchpad.AnyPerson"
+        attributes="linkBug
                     unlinkBug"/>
-        </class>
-        <class
-            class="lp.blueprints.model.specificationbug.SpecificationBug">
-            <allow
-                interface="lp.blueprints.interfaces.specificationbug.ISpecificationBug"/>
-        </class>
-        <subscriber
-            for="lp.blueprints.interfaces.specification.ISpecification                                        lazr.lifecycle.interfaces.IObjectCreatedEvent"
-            handler="canonical.launchpad.subscribers.karma.spec_created"/>
-        <subscriber
-            for="lp.blueprints.interfaces.specification.ISpecification                                        lazr.lifecycle.interfaces.IObjectModifiedEvent"
-            handler="canonical.launchpad.subscribers.karma.spec_modified"/>
-
-        <!-- these pages are simple enough they don't need browser code -->
-
-
-        <!-- these pages require special browser code -->
-
-
-        <!-- SpecificationSet -->
-
-        <class
-            class="lp.blueprints.model.specification.SpecificationSet">
-            <allow
-                interface="lp.blueprints.interfaces.specification.ISpecificationSet"/>
-        </class>
-        <securedutility
-            class="lp.blueprints.model.specification.SpecificationSet"
-            provides="lp.blueprints.interfaces.specification.ISpecificationSet">
-            <allow
-                interface="lp.blueprints.interfaces.specification.ISpecificationSet"/>
-        </securedutility>
-
-        <!-- SpecificationDelta -->
-
-        <class
-            class="lp.blueprints.adapters.SpecificationDelta">
-            <allow
-                interface="lp.blueprints.interfaces.specification.ISpecificationDelta"/>
-        </class>
-
-        <!-- SpecificationMessage -->
-
-        <class
-            class="lp.blueprints.model.specificationmessage.SpecificationMessage">
-            <allow
-                interface="lp.blueprints.interfaces.specificationmessage.ISpecificationMessage"/>
-        </class>
-
-        <!-- SpecificationMessageSet -->
-
-        <class
-            class="lp.blueprints.model.specificationmessage.SpecificationMessageSet">
-            <allow
-                interface="lp.blueprints.interfaces.specificationmessage.ISpecificationMessageSet"/>
-        </class>
-        <securedutility
-            class="lp.blueprints.model.specificationmessage.SpecificationMessageSet"
-            provides="lp.blueprints.interfaces.specificationmessage.ISpecificationMessageSet">
-            <allow
-                interface="lp.blueprints.interfaces.specificationmessage.ISpecificationMessageSet"/>
-        </securedutility>
-    </facet>
-    <subscriber
-        for="lp.blueprints.interfaces.specification.ISpecification                                 lazr.lifecycle.interfaces.IObjectModifiedEvent"
-        handler="lp.blueprints.subscribers.specification_goalstatus"/>
-    <subscriber
-        for="lp.blueprints.interfaces.specification.ISpecification                                 lazr.lifecycle.interfaces.IObjectModifiedEvent"
-        handler="canonical.launchpad.mailnotification.notify_specification_modified"/>
-    <adapter
-        name="blueprints"
-        provides="canonical.launchpad.webapp.interfaces.IBreadcrumb"
-        for="lp.blueprints.interfaces.specificationtarget.IHasSpecifications"
-        factory="lp.blueprints.browser.specificationtarget.BlueprintsVHostBreadcrumb"
-        permission="zope.Public"/>
-    <adapter
-        name="blueprints"
-        provides="canonical.launchpad.webapp.interfaces.IBreadcrumb"
-        for="lp.registry.interfaces.person.IPerson"
-        factory="lp.blueprints.browser.specificationtarget.BlueprintsVHostBreadcrumb"
-        permission="zope.Public"/>
+  </class>
+
+  <class class="lp.blueprints.model.specificationbug.SpecificationBug">
+    <allow interface="lp.blueprints.interfaces.specificationbug.ISpecificationBug"/>
+  </class>
+
+  <subscriber
+      for="lp.blueprints.interfaces.specification.ISpecification
+           lazr.lifecycle.interfaces.IObjectCreatedEvent"
+      handler="canonical.launchpad.subscribers.karma.spec_created"/>
+  <subscriber
+      for="lp.blueprints.interfaces.specification.ISpecification
+           lazr.lifecycle.interfaces.IObjectModifiedEvent"
+      handler="canonical.launchpad.subscribers.karma.spec_modified"/>
+  <subscriber
+      for="lp.blueprints.interfaces.specification.ISpecification
+           lazr.lifecycle.interfaces.IObjectModifiedEvent"
+      handler="lp.blueprints.subscribers.specification_update_lifecycle_status"/>
+  <subscriber
+      for="lp.blueprints.interfaces.specification.ISpecification
+           lazr.lifecycle.interfaces.IObjectModifiedEvent"
+      handler="lp.blueprints.subscribers.specification_goalstatus"/>
+  <subscriber
+      for="lp.blueprints.interfaces.specification.ISpecification
+           lazr.lifecycle.interfaces.IObjectModifiedEvent"
+      handler="canonical.launchpad.mailnotification.notify_specification_modified"/>
+
+  <!-- SpecificationSet -->
+
+  <class class="lp.blueprints.model.specification.SpecificationSet">
+    <allow interface="lp.blueprints.interfaces.specification.ISpecificationSet"/>
+  </class>
+
+  <securedutility
+      class="lp.blueprints.model.specification.SpecificationSet"
+      provides="lp.blueprints.interfaces.specification.ISpecificationSet">
+    <allow interface="lp.blueprints.interfaces.specification.ISpecificationSet"/>
+  </securedutility>
+
+  <!-- SpecificationDelta -->
+
+  <class class="lp.blueprints.adapters.SpecificationDelta">
+    <allow interface="lp.blueprints.interfaces.specification.ISpecificationDelta"/>
+  </class>
+
+  <!-- SpecificationMessage -->
+
+  <class class="lp.blueprints.model.specificationmessage.SpecificationMessage">
+    <allow interface="lp.blueprints.interfaces.specificationmessage.ISpecificationMessage"/>
+  </class>
+
+  <!-- SpecificationMessageSet -->
+
+  <class class="lp.blueprints.model.specificationmessage.SpecificationMessageSet">
+    <allow interface="lp.blueprints.interfaces.specificationmessage.ISpecificationMessageSet"/>
+  </class>
+
+  <securedutility
+      class="lp.blueprints.model.specificationmessage.SpecificationMessageSet"
+      provides="lp.blueprints.interfaces.specificationmessage.ISpecificationMessageSet">
+    <allow interface="lp.blueprints.interfaces.specificationmessage.ISpecificationMessageSet"/>
+  </securedutility>
+
 </configure>

=== modified file 'lib/lp/blueprints/interfaces/specification.py'
--- lib/lp/blueprints/interfaces/specification.py	2010-11-02 20:10:56 +0000
+++ lib/lp/blueprints/interfaces/specification.py	2010-11-05 19:52:03 +0000
@@ -41,6 +41,7 @@
     SpecificationDefinitionStatus,
     SpecificationGoalStatus,
     SpecificationImplementationStatus,
+    SpecificationLifecycleStatus,
     SpecificationPriority,
     )
 from lp.blueprints.interfaces.specificationtarget import IHasSpecifications
@@ -337,6 +338,12 @@
         'attribute, and also considers informational specs to be '
         'started when they are approved.')
 
+    lifecycle_status = Choice(
+        title=_('Lifecycle Status'),
+        vocabulary=SpecificationLifecycleStatus,
+        default=SpecificationLifecycleStatus.NOTSTARTED,
+        readonly=True)
+
     def retarget(product=None, distribution=None):
         """Retarget the spec to a new product or distribution. One of
         product or distribution must be None (but not both).

=== modified file 'lib/lp/blueprints/model/specification.py'
--- lib/lp/blueprints/model/specification.py	2010-11-02 20:10:56 +0000
+++ lib/lp/blueprints/model/specification.py	2010-11-05 19:52:03 +0000
@@ -12,7 +12,6 @@
 
 from lazr.lifecycle.event import (
     ObjectCreatedEvent,
-    ObjectDeletedEvent,
     ObjectModifiedEvent,
     )
 from lazr.lifecycle.objectdelta import ObjectDelta
@@ -24,11 +23,7 @@
     SQLRelatedJoin,
     StringCol,
     )
-from storm.expr import (
-    LeftJoin,
-    )
 from storm.locals import (
-    ClassAlias,
     Desc,
     SQL,
     )
@@ -386,6 +381,16 @@
                     (self.definition_status ==
                      SpecificationDefinitionStatus.APPROVED)))
 
+    @property
+    def lifecycle_status(self):
+        """Combine the is_complete and is_started emergent properties."""
+        if self.is_complete:
+            return SpecificationLifecycleStatus.COMPLETE
+        elif self.is_started:
+            return SpecificationLifecycleStatus.STARTED
+        else:
+            return SpecificationLifecycleStatus.NOTSTARTED
+
     def updateLifecycleStatus(self, user):
         """See ISpecification."""
         newstatus = None

=== modified file 'lib/lp/blueprints/subscribers.py'
--- lib/lp/blueprints/subscribers.py	2010-11-01 03:43:58 +0000
+++ lib/lp/blueprints/subscribers.py	2010-11-05 19:52:03 +0000
@@ -18,3 +18,13 @@
         return
     if delta.productseries is not None or delta.distroseries is not None:
         spec.goalstatus = SpecificationGoalStatus.PROPOSED
+
+
+def specification_update_lifecycle_status(spec, event):
+    """Mark the specification as started and/or complete if appropriate.
+
+    Does nothing if there is no user associated with the event.
+    """
+    if event.user is None:
+        return
+    spec.updateLifecycleStatus(IPerson(event.user))

=== modified file 'lib/lp/bugs/interfaces/bugtask.py'
--- lib/lp/bugs/interfaces/bugtask.py	2010-11-03 14:31:33 +0000
+++ lib/lp/bugs/interfaces/bugtask.py	2010-11-05 19:52:03 +0000
@@ -1188,8 +1188,8 @@
         self.hardware_is_linked_to_bug = hardware_is_linked_to_bug
         self.linked_branches = linked_branches
         self.structural_subscriber = structural_subscriber
-        self.modified_since = None
-        self.created_since = None
+        self.modified_since = modified_since
+        self.created_since = created_since
 
     def setProduct(self, product):
         """Set the upstream context on which to filter the search."""

=== modified file 'lib/lp/bugs/mail/bugnotificationrecipients.py'
--- lib/lp/bugs/mail/bugnotificationrecipients.py	2010-08-20 20:31:18 +0000
+++ lib/lp/bugs/mail/bugnotificationrecipients.py	2010-11-05 19:52:03 +0000
@@ -10,7 +10,7 @@
 
 from zope.interface import implements
 
-from canonical.launchpad.interfaces import INotificationRecipientSet
+from canonical.launchpad.interfaces.launchpad import INotificationRecipientSet
 from lp.services.mail.basemailer import RecipientReason
 from lp.services.mail.notificationrecipientset import NotificationRecipientSet
 

=== modified file 'lib/lp/bugs/scripts/checkwatches/core.py'
--- lib/lp/bugs/scripts/checkwatches/core.py	2010-10-26 15:47:24 +0000
+++ lib/lp/bugs/scripts/checkwatches/core.py	2010-11-05 19:52:03 +0000
@@ -36,15 +36,7 @@
 from zope.component import getUtility
 
 from canonical.database.sqlbase import flush_database_updates
-from canonical.launchpad.interfaces import (
-    CreateBugParams,
-    IBugTrackerSet,
-    IBugWatchSet,
-    IDistribution,
-    ILaunchpadCelebrities,
-    IPersonSet,
-    PersonCreationRationale,
-    )
+from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
 from canonical.launchpad.scripts.logger import log as default_log
 from lp.bugs import externalbugtracker
 from lp.bugs.externalbugtracker import (
@@ -52,6 +44,9 @@
     BugWatchUpdateError,
     UnknownBugTrackerTypeError,
     )
+from lp.bugs.interfaces.bug import CreateBugParams
+from lp.bugs.interfaces.bugtracker import IBugTrackerSet
+from lp.bugs.interfaces.bugwatch import IBugWatchSet
 from lp.bugs.scripts.checkwatches.base import (
     commit_before,
     with_interaction,
@@ -61,6 +56,11 @@
 from lp.bugs.scripts.checkwatches.utilities import (
     get_bugwatcherrortype_for_error,
     )
+from lp.registry.interfaces.distribution import IDistribution
+from lp.registry.interfaces.person import (
+    IPersonSet,
+    PersonCreationRationale,
+    )
 from lp.services.database.bulk import reload
 from lp.services.scripts.base import LaunchpadCronScript
 

=== modified file 'lib/lp/bugs/scripts/checkwatches/scheduler.py'
--- lib/lp/bugs/scripts/checkwatches/scheduler.py	2010-10-03 15:30:06 +0000
+++ lib/lp/bugs/scripts/checkwatches/scheduler.py	2010-11-05 19:52:03 +0000
@@ -11,7 +11,7 @@
 import transaction
 
 from canonical.database.sqlbase import sqlvalues
-from canonical.launchpad.interfaces import IMasterStore
+from canonical.launchpad.interfaces.lpstorm import IMasterStore
 from canonical.launchpad.utilities.looptuner import TunableLoop
 from lp.bugs.interfaces.bugwatch import BUG_WATCH_ACTIVITY_SUCCESS_STATUSES
 from lp.bugs.model.bugwatch import BugWatch

=== modified file 'lib/lp/bugs/tests/test_bugtask_search.py'
--- lib/lp/bugs/tests/test_bugtask_search.py	2010-10-29 13:00:57 +0000
+++ lib/lp/bugs/tests/test_bugtask_search.py	2010-11-05 19:52:03 +0000
@@ -25,6 +25,7 @@
 
 from lp.bugs.interfaces.bugattachment import BugAttachmentType
 from lp.bugs.interfaces.bugtask import (
+    BugBranchSearch,
     BugTaskImportance,
     BugTaskSearchParams,
     BugTaskStatus,
@@ -53,29 +54,30 @@
         super(SearchTestBase, self).setUp()
         self.bugtask_set = getUtility(IBugTaskSet)
 
+    def assertSearchFinds(self, params, expected_bugtasks):
+        # Run a search for the given search parameters and check if
+        # the result matches the expected bugtasks.
+        search_result = self.runSearch(params)
+        expected = self.resultValuesForBugtasks(expected_bugtasks)
+        self.assertEqual(expected, search_result)
+
     def test_search_all_bugtasks_for_target(self):
         # BugTaskSet.search() returns all bug tasks for a given bug
         # target, if only the bug target is passed as a search parameter.
         params = self.getBugTaskSearchParams(user=None)
-        search_result = self.runSearch(params)
-        expected = self.resultValuesForBugtasks(self.bugtasks)
-        self.assertEqual(expected, search_result)
+        self.assertSearchFinds(params, self.bugtasks)
 
     def test_private_bug_in_search_result(self):
         # Private bugs are not included in search results for anonymous users.
         with person_logged_in(self.owner):
             self.bugtasks[-1].bug.setPrivate(True, self.owner)
         params = self.getBugTaskSearchParams(user=None)
-        search_result = self.runSearch(params)
-        expected = self.resultValuesForBugtasks(self.bugtasks)[:-1]
-        self.assertEqual(expected, search_result)
+        self.assertSearchFinds(params, self.bugtasks[:-1])
 
         # Private bugs are not included in search results for ordinary users.
         user = self.factory.makePerson()
         params = self.getBugTaskSearchParams(user=user)
-        search_result = self.runSearch(params)
-        expected = self.resultValuesForBugtasks(self.bugtasks)[:-1]
-        self.assertEqual(expected, search_result)
+        self.assertSearchFinds(params, self.bugtasks[:-1])
 
         # If the user is subscribed to the bug, it is included in the
         # search result.
@@ -83,16 +85,12 @@
         with person_logged_in(self.owner):
             self.bugtasks[-1].bug.subscribe(user, self.owner)
         params = self.getBugTaskSearchParams(user=user)
-        search_result = self.runSearch(params)
-        expected = self.resultValuesForBugtasks(self.bugtasks)
-        self.assertEqual(expected, search_result)
+        self.assertSearchFinds(params, self.bugtasks)
 
         # Private bugs are included in search results for admins.
         admin = getUtility(IPersonSet).getByEmail('foo.bar@xxxxxxxxxxxxx')
         params = self.getBugTaskSearchParams(user=admin)
-        search_result = self.runSearch(params)
-        expected = self.resultValuesForBugtasks(self.bugtasks)
-        self.assertEqual(expected, search_result)
+        self.assertSearchFinds(params, self.bugtasks)
 
     def test_search_by_bug_reporter(self):
         # Search results can be limited to bugs filed by a given person.
@@ -100,9 +98,7 @@
         reporter = bugtask.bug.owner
         params = self.getBugTaskSearchParams(
             user=None, bug_reporter=reporter)
-        search_result = self.runSearch(params)
-        expected = self.resultValuesForBugtasks([bugtask])
-        self.assertEqual(expected, search_result)
+        self.assertSearchFinds(params, [bugtask])
 
     def test_search_by_bug_commenter(self):
         # Search results can be limited to bugs having a comment from a
@@ -118,9 +114,7 @@
             expected.bug.newMessage(owner=commenter, content='a comment')
         params = self.getBugTaskSearchParams(
             user=None, bug_commenter=commenter)
-        search_result = self.runSearch(params)
-        expected = self.resultValuesForBugtasks([expected])
-        self.assertEqual(expected, search_result)
+        self.assertSearchFinds(params, [expected])
 
     def test_search_by_person_affected_by_bug(self):
         # Search results can be limited to bugs which affect a given person.
@@ -130,9 +124,7 @@
             expected.bug.markUserAffected(affected_user)
         params = self.getBugTaskSearchParams(
             user=None, affected_user=affected_user)
-        search_result = self.runSearch(params)
-        expected = self.resultValuesForBugtasks([expected])
-        self.assertEqual(expected, search_result)
+        self.assertSearchFinds(params, [expected])
 
     def test_search_by_bugtask_assignee(self):
         # Search results can be limited to bugtask assigned to a given
@@ -142,9 +134,7 @@
         with person_logged_in(assignee):
             expected.transitionToAssignee(assignee)
         params = self.getBugTaskSearchParams(user=None, assignee=assignee)
-        search_result = self.runSearch(params)
-        expected = self.resultValuesForBugtasks([expected])
-        self.assertEqual(expected, search_result)
+        self.assertSearchFinds(params, [expected])
 
     def test_search_by_bug_subscriber(self):
         # Search results can be limited to bugs to which a given person
@@ -154,9 +144,69 @@
         with person_logged_in(subscriber):
             expected.bug.subscribe(subscriber, subscribed_by=subscriber)
         params = self.getBugTaskSearchParams(user=None, subscriber=subscriber)
-        search_result = self.runSearch(params)
-        expected = self.resultValuesForBugtasks([expected])
-        self.assertEqual(expected, search_result)
+        self.assertSearchFinds(params, [expected])
+
+    def subscribeToTarget(self, subscriber):
+        # Subscribe the given person to the search target.
+        with person_logged_in(subscriber):
+            self.searchtarget.addSubscription(
+                subscriber, subscribed_by=subscriber)
+
+    def _findBugtaskForOtherProduct(self, bugtask, main_product):
+        # Return the bugtask for the product that is not related to the
+        # main bug target.
+        #
+        # The default bugtasks of this test suite are created by
+        # ObjectFactory.makeBugTask() as follows:
+        # - a new bug is created having a new product as the target.
+        # - another bugtask is created for self.searchtarget (or,
+        #   when self.searchtarget is a milestone, for the product
+        #   of the milestone)
+        # This method returns the bug task for the product that is not
+        # related to the main bug target.
+        bug = bugtask.bug
+        for other_task in bug.bugtasks:
+            other_target = other_task.target
+            if (IProduct.providedBy(other_target)
+                and other_target != main_product):
+                return other_task
+        self.fail(
+            'No bug task found for a product that is not the target of '
+            'the main test bugtask.')
+
+    def findBugtaskForOtherProduct(self, bugtask):
+        # Return the bugtask for the product that is not related to the
+        # main bug target.
+        #
+        # This method must ober overridden for product related tests.
+        return self._findBugtaskForOtherProduct(bugtask, None)
+
+    def test_search_by_structural_subscriber(self):
+        # Search results can be limited to bugs with a bug target to which
+        # a given person has a structural subscription.
+        subscriber = self.factory.makePerson()
+        # If the given person is not subscribed, no bugtasks are returned.
+        params = self.getBugTaskSearchParams(
+            user=None, structural_subscriber=subscriber)
+        self.assertSearchFinds(params, [])
+        # When the person is subscribed, all bugtasks are returned.
+        self.subscribeToTarget(subscriber)
+        params = self.getBugTaskSearchParams(
+            user=None, structural_subscriber=subscriber)
+        self.assertSearchFinds(params, self.bugtasks)
+
+        # Searching for a structural subscriber does not return a bugtask,
+        # if the person is subscribed to another target than the main
+        # bug target.
+        other_subscriber = self.factory.makePerson()
+        other_bugtask = self.findBugtaskForOtherProduct(self.bugtasks[0])
+        other_target = other_bugtask.target
+        with person_logged_in(other_subscriber):
+            other_target.addSubscription(
+                other_subscriber, subscribed_by=other_subscriber)
+        params = self.getBugTaskSearchParams(
+            user=None, structural_subscriber=other_subscriber)
+        self.assertSearchFinds(params, [])
 
     def test_search_by_bug_attachment(self):
         # Search results can be limited to bugs having attachments of
@@ -171,23 +221,17 @@
         # We can search for bugs with non-patch attachments...
         params = self.getBugTaskSearchParams(
             user=None, attachmenttype=BugAttachmentType.UNSPECIFIED)
-        search_result = self.runSearch(params)
-        expected = self.resultValuesForBugtasks(self.bugtasks[:1])
-        self.assertEqual(expected, search_result)
+        self.assertSearchFinds(params, self.bugtasks[:1])
         # ... for bugs with patches...
         params = self.getBugTaskSearchParams(
             user=None, attachmenttype=BugAttachmentType.PATCH)
-        search_result = self.runSearch(params)
-        expected = self.resultValuesForBugtasks(self.bugtasks[1:2])
-        self.assertEqual(expected, search_result)
+        self.assertSearchFinds(params, self.bugtasks[1:2])
         # and for bugs with patches or attachments
         params = self.getBugTaskSearchParams(
             user=None, attachmenttype=any(
                 BugAttachmentType.PATCH,
                 BugAttachmentType.UNSPECIFIED))
-        search_result = self.runSearch(params)
-        expected = self.resultValuesForBugtasks(self.bugtasks[:2])
-        self.assertEqual(expected, search_result)
+        self.assertSearchFinds(params, self.bugtasks[:2])
 
     def setUpFullTextSearchTests(self):
         # Set text fields indexed by Bug.fti, BugTask.fti or
@@ -205,40 +249,30 @@
         self.setUpFullTextSearchTests()
         params = self.getBugTaskSearchParams(
             user=None, searchtext='one title')
-        search_result = self.runSearch(params)
-        expected = self.resultValuesForBugtasks(self.bugtasks[:1])
-        self.assertEqual(expected, search_result)
+        self.assertSearchFinds(params, self.bugtasks[:1])
         # ... by BugTask.fti ...
         params = self.getBugTaskSearchParams(
             user=None, searchtext='two explanation')
-        search_result = self.runSearch(params)
-        expected = self.resultValuesForBugtasks(self.bugtasks[1:2])
-        self.assertEqual(expected, search_result)
+        self.assertSearchFinds(params, self.bugtasks[1:2])
         # ...and by MessageChunk.fti
         params = self.getBugTaskSearchParams(
             user=None, searchtext='three comment')
-        search_result = self.runSearch(params)
-        expected = self.resultValuesForBugtasks(self.bugtasks[2:3])
-        self.assertEqual(expected, search_result)
+        self.assertSearchFinds(params, self.bugtasks[2:3])
 
     def test_fast_fulltext_search(self):
         # Fast full text searches find text indexed by Bug.fti...
         self.setUpFullTextSearchTests()
         params = self.getBugTaskSearchParams(
             user=None, fast_searchtext='one title')
-        search_result = self.runSearch(params)
-        expected = self.resultValuesForBugtasks(self.bugtasks[:1])
-        self.assertEqual(expected, search_result)
+        self.assertSearchFinds(params, self.bugtasks[:1])
         # ... but not text indexed by BugTask.fti ...
         params = self.getBugTaskSearchParams(
             user=None, fast_searchtext='two explanation')
-        search_result = self.runSearch(params)
-        self.assertEqual([], search_result)
+        self.assertSearchFinds(params, [])
         # ..or by MessageChunk.fti
         params = self.getBugTaskSearchParams(
             user=None, fast_searchtext='three comment')
-        search_result = self.runSearch(params)
-        self.assertEqual([], search_result)
+        self.assertSearchFinds(params, [])
 
     def test_has_no_upstream_bugtask(self):
         # Search results can be limited to bugtasks of bugs that do
@@ -258,13 +292,13 @@
             IDistributionSourcePackage.providedBy(self.searchtarget)):
             if IDistribution.providedBy(self.searchtarget):
                 bug = self.factory.makeBug(distribution=self.searchtarget)
-                expected = self.resultValuesForBugtasks([bug.default_bugtask])
+                expected = [bug.default_bugtask]
             else:
                 bug = self.factory.makeBug(
                     distribution=self.searchtarget.distribution)
                 bugtask = self.factory.makeBugTask(
                     bug=bug, target=self.searchtarget)
-                expected = self.resultValuesForBugtasks([bugtask])
+                expected = [bugtask]
         else:
             # Bugs without distribution related bugtasks have always at
             # least one product related bugtask, hence a
@@ -273,23 +307,14 @@
             expected = []
         params = self.getBugTaskSearchParams(
             user=None, has_no_upstream_bugtask=True)
-        search_result = self.runSearch(params)
-        self.assertEqual(expected, search_result)
+        self.assertSearchFinds(params, expected)
 
     def changeStatusOfBugTaskForOtherProduct(self, bugtask, new_status):
         # Change the status of another bugtask of the same bug to the
         # given status.
-        bug = bugtask.bug
-        for other_task in bug.bugtasks:
-            other_target = other_task.target
-            if other_task != bugtask and IProduct.providedBy(other_target):
-                with person_logged_in(other_target.owner):
-                    other_task.transitionToStatus(
-                        new_status, other_target.owner)
-                return
-        self.fail(
-            'No bug task found for a product that is not the target of '
-            'the main test bugtask.')
+        other_task = self.findBugtaskForOtherProduct(bugtask)
+        with person_logged_in(other_task.target.owner):
+            other_task.transitionToStatus(new_status, other_task.target.owner)
 
     def test_upstream_status(self):
         # Search results can be filtered by the status of an upstream
@@ -299,14 +324,11 @@
         # with status NEW for the "other" product, hence all bug tasks
         # will be returned in a search for bugs that are open upstream.
         params = self.getBugTaskSearchParams(user=None, open_upstream=True)
-        search_result = self.runSearch(params)
-        expected = self.resultValuesForBugtasks(self.bugtasks)
-        self.assertEqual(expected, search_result)
+        self.assertSearchFinds(params, self.bugtasks)
         # A search for tasks resolved upstream does not yield any bugtask.
         params = self.getBugTaskSearchParams(
             user=None, resolved_upstream=True)
-        search_result = self.runSearch(params)
-        self.assertEqual([], search_result)
+        self.assertSearchFinds(params, [])
         # But if we set upstream bug tasks to "fix committed" or "fix
         # released", the related bug tasks for our test target appear in
         # the search result.
@@ -314,14 +336,11 @@
             self.bugtasks[0], BugTaskStatus.FIXCOMMITTED)
         self.changeStatusOfBugTaskForOtherProduct(
             self.bugtasks[1], BugTaskStatus.FIXRELEASED)
-        search_result = self.runSearch(params)
-        expected = self.resultValuesForBugtasks(self.bugtasks[:2])
-        self.assertEqual(expected, search_result)
+        self.assertSearchFinds(params, self.bugtasks[:2])
         # A search for bug tasks open upstream now returns only one
         # test task.
         params = self.getBugTaskSearchParams(user=None, open_upstream=True)
-        search_result = self.runSearch(params)
-        expected = self.resultValuesForBugtasks(self.bugtasks[2:])
+        self.assertSearchFinds(params, self.bugtasks[2:])
 
     def test_tags(self):
         # Search results can be limited to bugs having given tags.
@@ -330,44 +349,31 @@
             self.bugtasks[1].bug.tags = ['tag1', 'tag3']
         params = self.getBugTaskSearchParams(
             user=None, tag=any('tag2', 'tag3'))
-        search_result = self.runSearch(params)
-        expected = self.resultValuesForBugtasks(self.bugtasks[:2])
-        self.assertEqual(expected, search_result)
+        self.assertSearchFinds(params, self.bugtasks[:2])
 
         params = self.getBugTaskSearchParams(
             user=None, tag=all('tag2', 'tag3'))
-        search_result = self.runSearch(params)
-        self.assertEqual([], search_result)
+        self.assertSearchFinds(params, [])
 
         params = self.getBugTaskSearchParams(
             user=None, tag=all('tag1', 'tag3'))
-        search_result = self.runSearch(params)
-        expected = self.resultValuesForBugtasks(self.bugtasks[1:2])
-        self.assertEqual(expected, search_result)
+        self.assertSearchFinds(params, self.bugtasks[1:2])
 
         params = self.getBugTaskSearchParams(
             user=None, tag=all('tag1', '-tag3'))
-        search_result = self.runSearch(params)
-        expected = self.resultValuesForBugtasks(self.bugtasks[:1])
-        self.assertEqual(expected, search_result)
+        self.assertSearchFinds(params, self.bugtasks[:1])
 
         params = self.getBugTaskSearchParams(
             user=None, tag=all('-tag1'))
-        search_result = self.runSearch(params)
-        expected = self.resultValuesForBugtasks(self.bugtasks[2:])
-        self.assertEqual(expected, search_result)
+        self.assertSearchFinds(params, self.bugtasks[2:])
 
         params = self.getBugTaskSearchParams(
             user=None, tag=all('*'))
-        search_result = self.runSearch(params)
-        expected = self.resultValuesForBugtasks(self.bugtasks[:2])
-        self.assertEqual(expected, search_result)
+        self.assertSearchFinds(params, self.bugtasks[:2])
 
         params = self.getBugTaskSearchParams(
             user=None, tag=all('-*'))
-        search_result = self.runSearch(params)
-        expected = self.resultValuesForBugtasks(self.bugtasks[2:])
-        self.assertEqual(expected, search_result)
+        self.assertSearchFinds(params, self.bugtasks[2:])
 
     def test_date_closed(self):
         # Search results can be filtered by the date_closed time
@@ -379,13 +385,106 @@
         self.assertTrue(utc_now >= self.bugtasks[2].date_closed)
         params = self.getBugTaskSearchParams(
             user=None, date_closed=greater_than(utc_now-timedelta(days=1)))
-        search_result = self.runSearch(params)
-        expected = self.resultValuesForBugtasks(self.bugtasks[2:])
-        self.assertEqual(expected, search_result)
+        self.assertSearchFinds(params, self.bugtasks[2:])
         params = self.getBugTaskSearchParams(
             user=None, date_closed=greater_than(utc_now+timedelta(days=1)))
-        search_result = self.runSearch(params)
-        self.assertEqual([], search_result)
+        self.assertSearchFinds(params, [])
+
+    def test_created_since(self):
+        # Search results can be limited to bugtasks created after a
+        # given time.
+        one_day_ago = self.bugtasks[0].datecreated - timedelta(days=1)
+        two_days_ago = self.bugtasks[0].datecreated - timedelta(days=2)
+        with person_logged_in(self.owner):
+            self.bugtasks[0].datecreated = two_days_ago
+        params = self.getBugTaskSearchParams(
+            user=None, created_since=one_day_ago)
+        self.assertSearchFinds(params, self.bugtasks[1:])
+
+    def test_modified_since(self):
+        # Search results can be limited to bugs modified after a
+        # given time.
+        one_day_ago = (
+            self.bugtasks[0].bug.date_last_updated - timedelta(days=1))
+        two_days_ago = (
+            self.bugtasks[0].bug.date_last_updated - timedelta(days=2))
+        with person_logged_in(self.owner):
+            self.bugtasks[0].bug.date_last_updated = two_days_ago
+        params = self.getBugTaskSearchParams(
+            user=None, modified_since=one_day_ago)
+        self.assertSearchFinds(params, self.bugtasks[1:])
+
+    def test_branches_linked(self):
+        # Search results can be limited to bugs with or without linked
+        # branches.
+        with person_logged_in(self.owner):
+            branch = self.factory.makeBranch()
+            self.bugtasks[0].bug.linkBranch(branch, self.owner)
+        params = self.getBugTaskSearchParams(
+            user=None, linked_branches=BugBranchSearch.BUGS_WITH_BRANCHES)
+        self.assertSearchFinds(params, self.bugtasks[:1])
+        params = self.getBugTaskSearchParams(
+            user=None, linked_branches=BugBranchSearch.BUGS_WITHOUT_BRANCHES)
+        self.assertSearchFinds(params, self.bugtasks[1:])
+
+    def test_limit_search_to_one_bug(self):
+        # Search results can be limited to a given bug.
+        params = self.getBugTaskSearchParams(
+            user=None, bug=self.bugtasks[0].bug)
+        self.assertSearchFinds(params, self.bugtasks[:1])
+        other_bug = self.factory.makeBug()
+        params = self.getBugTaskSearchParams(user=None, bug=other_bug)
+        self.assertSearchFinds(params, [])
+
+    def test_filter_by_status(self):
+        # Search results can be limited to bug tasks with a given status.
+        params = self.getBugTaskSearchParams(
+            user=None, status=BugTaskStatus.FIXCOMMITTED)
+        self.assertSearchFinds(params, self.bugtasks[2:])
+        params = self.getBugTaskSearchParams(
+            user=None, status=any(BugTaskStatus.NEW, BugTaskStatus.TRIAGED))
+        self.assertSearchFinds(params, self.bugtasks[:2])
+        params = self.getBugTaskSearchParams(
+            user=None, status=BugTaskStatus.WONTFIX)
+        self.assertSearchFinds(params, [])
+
+    def test_filter_by_importance(self):
+        # Search results can be limited to bug tasks with a given importance.
+        params = self.getBugTaskSearchParams(
+            user=None, importance=BugTaskImportance.HIGH)
+        self.assertSearchFinds(params, self.bugtasks[:1])
+        params = self.getBugTaskSearchParams(
+            user=None,
+            importance=any(BugTaskImportance.HIGH, BugTaskImportance.LOW))
+        self.assertSearchFinds(params, self.bugtasks[:2])
+        params = self.getBugTaskSearchParams(
+            user=None, importance=BugTaskImportance.MEDIUM)
+        self.assertSearchFinds(params, [])
+
+    def test_omit_duplicate_bugs(self):
+        # Duplicate bugs can optionally be excluded from search results.
+        # The default behaviour is to include duplicates.
+        duplicate_bug = self.bugtasks[0].bug
+        master_bug = self.bugtasks[1].bug
+        with person_logged_in(self.owner):
+            duplicate_bug.markAsDuplicate(master_bug)
+        params = self.getBugTaskSearchParams(user=None)
+        self.assertSearchFinds(params, self.bugtasks)
+        # If we explicitly pass the parameter omit_duplicates=False, we get
+        # the same result.
+        params = self.getBugTaskSearchParams(user=None, omit_dupes=False)
+        self.assertSearchFinds(params, self.bugtasks)
+        # If omit_duplicates is set to True, the first task bug is omitted.
+        params = self.getBugTaskSearchParams(user=None, omit_dupes=True)
+        self.assertSearchFinds(params, self.bugtasks[1:])
+
+    def test_has_cve(self):
+        # Search results can be limited to bugs linked to a CVE.
+        with person_logged_in(self.owner):
+            cve = self.factory.makeCVE('2010-0123')
+            self.bugtasks[0].bug.linkCVE(cve, self.owner)
+        params = self.getBugTaskSearchParams(user=None, has_cve=True)
+        self.assertSearchFinds(params, self.bugtasks[:1])
 
 
 class ProductAndDistributionTests:
@@ -405,9 +504,7 @@
             self.bugtasks[0].bug.addNomination(nominator, series1)
             self.bugtasks[1].bug.addNomination(nominator, series2)
         params = self.getBugTaskSearchParams(user=None, nominated_for=series1)
-        search_result = self.runSearch(params)
-        expected = self.resultValuesForBugtasks(self.bugtasks[:1])
-        self.assertEqual(expected, search_result)
+        self.assertSearchFinds(params, self.bugtasks[:1])
 
 
 class BugTargetTestBase:
@@ -445,15 +542,12 @@
         supervisor = self.factory.makeTeam(owner=self.owner)
         params = self.getBugTaskSearchParams(
             user=None, bug_supervisor=supervisor)
-        search_result = self.runSearch(params)
-        self.assertEqual([], search_result)
+        self.assertSearchFinds(params, [])
 
         # If we appoint a bug supervisor, searching for bug tasks
         # by supervisor will return all bugs for our test target.
         self.setSupervisor(supervisor)
-        search_result = self.runSearch(params)
-        expected = self.resultValuesForBugtasks(self.bugtasks)
-        self.assertEqual(expected, search_result)
+        self.assertSearchFinds(params, self.bugtasks)
 
     def setSupervisor(self, supervisor):
         """Set the bug supervisor for the bug task target."""
@@ -484,6 +578,11 @@
         """See `ProductAndDistributionTests`."""
         return self.factory.makeProductSeries(product=self.searchtarget)
 
+    def findBugtaskForOtherProduct(self, bugtask):
+        # Return the bugtask for the product that is not related to the
+        # main bug target.
+        return self._findBugtaskForOtherProduct(bugtask, self.searchtarget)
+
 
 class ProductSeriesTarget(BugTargetTestBase):
     """Use a product series as the bug target."""
@@ -503,6 +602,31 @@
         params.setProductSeries(self.searchtarget)
         return params
 
+    def changeStatusOfBugTaskForOtherProduct(self, bugtask, new_status):
+        # Change the status of another bugtask of the same bug to the
+        # given status.
+        #
+        # This method is called by SearchTestBase.test_upstream_status().
+        # A search for bugs which are open or closed upstream has an
+        # odd behaviour when the search target is a product series: In
+        # this case, all bugs with an open or closed bug task for _any_
+        # product are returned, including bug tasks for the main product
+        # of the series. Hence we must set the status for all products
+        # in order to avoid a failure of test_upstream_status().
+        bug = bugtask.bug
+        for other_task in bugtask.related_tasks:
+            other_target = other_task.target
+            if IProduct.providedBy(other_target):
+                with person_logged_in(other_target.owner):
+                    other_task.transitionToStatus(
+                        new_status, other_target.owner)
+
+    def findBugtaskForOtherProduct(self, bugtask):
+        # Return the bugtask for the product that not related to the
+        # main bug target.
+        return self._findBugtaskForOtherProduct(
+            bugtask, self.searchtarget.product)
+
 
 class ProjectGroupTarget(BugTargetTestBase, BugTargetWithBugSuperVisor):
     """Use a project group as the bug target."""
@@ -525,8 +649,10 @@
     def makeBugTasks(self):
         """Create bug tasks for the search target."""
         self.bugtasks = []
+        self.products = []
         with person_logged_in(self.owner):
             product = self.factory.makeProduct(owner=self.owner)
+            self.products.append(product)
             product.project = self.searchtarget
             self.bugtasks.append(
                 self.factory.makeBugTask(target=product))
@@ -535,6 +661,7 @@
                 BugTaskStatus.TRIAGED, self.owner)
 
             product = self.factory.makeProduct(owner=self.owner)
+            self.products.append(product)
             product.project = self.searchtarget
             self.bugtasks.append(
                 self.factory.makeBugTask(target=product))
@@ -543,6 +670,7 @@
             BugTaskStatus.NEW, self.owner)
 
             product = self.factory.makeProduct(owner=self.owner)
+            self.products.append(product)
             product.project = self.searchtarget
             self.bugtasks.append(
                 self.factory.makeBugTask(target=product))
@@ -557,6 +685,19 @@
             for bugtask in self.bugtasks:
                 bugtask.target.setBugSupervisor(supervisor, self.owner)
 
+    def findBugtaskForOtherProduct(self, bugtask):
+        # Return the bugtask for the product that not related to the
+        # main bug target.
+        bug = bugtask.bug
+        for other_task in bug.bugtasks:
+            other_target = other_task.target
+            if (IProduct.providedBy(other_target)
+                and other_target not in self.products):
+                return other_task
+        self.fail(
+            'No bug task found for a product that is not the target of '
+            'the main test bugtask.')
+
 
 class MilestoneTarget(BugTargetTestBase):
     """Use a milestone as the bug target."""
@@ -583,6 +724,11 @@
             for bugtask in self.bugtasks:
                 bugtask.transitionToMilestone(self.searchtarget, self.owner)
 
+    def findBugtaskForOtherProduct(self, bugtask):
+        # Return the bugtask for the product that not related to the
+        # main bug target.
+        return self._findBugtaskForOtherProduct(bugtask, self.product)
+
 
 class DistributionTarget(BugTargetTestBase, ProductAndDistributionTests,
                          BugTargetWithBugSuperVisor):
@@ -645,6 +791,14 @@
         params.setSourcePackage(self.searchtarget)
         return params
 
+    def subscribeToTarget(self, subscriber):
+        # Subscribe the given person to the search target.
+        # Source packages do not support structural subscriptions,
+        # so we subscribe to the distro series instead.
+        with person_logged_in(subscriber):
+            self.searchtarget.distroseries.addSubscription(
+                subscriber, subscribed_by=subscriber)
+
 
 class DistributionSourcePackageTarget(BugTargetTestBase,
                                       BugTargetWithBugSuperVisor):

=== modified file 'lib/lp/code/browser/branch.py'
--- lib/lp/code/browser/branch.py	2010-11-04 19:59:02 +0000
+++ lib/lp/code/browser/branch.py	2010-11-05 19:52:03 +0000
@@ -140,6 +140,7 @@
 from lp.code.interfaces.branchnamespace import IBranchNamespacePolicy
 from lp.code.interfaces.branchtarget import IBranchTarget
 from lp.code.interfaces.codereviewvote import ICodeReviewVoteReference
+from lp.code.interfaces.sourcepackagerecipe import recipes_enabled
 from lp.registry.interfaces.person import (
     IPerson,
     IPersonSet,
@@ -382,7 +383,7 @@
             '+upgrade', 'Upgrade this branch', icon='edit', enabled=enabled)
 
     def create_recipe(self):
-        if not self.context.private and config.build_from_branch.enabled:
+        if not self.context.private and recipes_enabled():
             enabled = True
         else:
             enabled = False

=== modified file 'lib/lp/code/browser/configure.zcml'
--- lib/lp/code/browser/configure.zcml	2010-10-26 21:37:42 +0000
+++ lib/lp/code/browser/configure.zcml	2010-11-05 19:52:03 +0000
@@ -1262,7 +1262,7 @@
             permission="launchpad.AnyPerson"
             facet="branches"
             name="+new-recipe"
-            template="../../app/templates/generic-edit.pt"/>
+            template="../templates/sourcepackagerecipe-new.pt"/>
         <browser:page
             for="lp.code.interfaces.sourcepackagerecipe.ISourcePackageRecipe"
             layer="lp.code.publisher.CodeLayer"

=== modified file 'lib/lp/code/browser/sourcepackagerecipelisting.py'
--- lib/lp/code/browser/sourcepackagerecipelisting.py	2010-08-23 02:07:45 +0000
+++ lib/lp/code/browser/sourcepackagerecipelisting.py	2010-11-05 19:52:03 +0000
@@ -20,6 +20,7 @@
     Link,
     )
 from lp.code.interfaces.branch import IBranch
+from lp.code.interfaces.sourcepackagerecipe import recipes_enabled
 from lp.registry.interfaces.person import IPerson
 from lp.registry.interfaces.product import IProduct
 
@@ -32,7 +33,7 @@
         enabled = False
         if self.context.getRecipes().count():
             enabled = True
-        if not config.build_from_branch.enabled:
+        if not recipes_enabled():
             enabled = False
         return Link(
             '+recipes', text, icon='info', enabled=enabled, site='code')

=== modified file 'lib/lp/code/browser/tests/test_sourcepackagerecipe.py'
--- lib/lp/code/browser/tests/test_sourcepackagerecipe.py	2010-10-29 00:06:49 +0000
+++ lib/lp/code/browser/tests/test_sourcepackagerecipe.py	2010-11-05 19:52:03 +0000
@@ -121,7 +121,7 @@
         browser.getControl(name='field.name').value = 'daily'
         browser.getControl('Description').value = 'Make some food!'
         browser.getControl('Secret Squirrel').click()
-        browser.getControl('Build daily').click()
+        browser.getControl('Automatically build each day').click()
         browser.getControl('Create Recipe').click()
 
         pattern = """\
@@ -184,7 +184,7 @@
         browser.getControl(name='field.name').value = 'daily'
         browser.getControl('Description').value = 'Make some food!'
         browser.getControl('Secret Squirrel').click()
-        browser.getControl('Build daily').click()
+        browser.getControl('Automatically build each day').click()
         browser.getControl('Owner').displayValue = ['Good Chefs']
         browser.getControl('Create Recipe').click()
 
@@ -279,7 +279,7 @@
         browser.getControl(name='field.name').value = 'daily'
         browser.getControl('Description').value = 'Make some food!'
 
-        browser.getControl('Build daily').click()
+        browser.getControl('Automatically build each day').click()
         browser.getControl('Create Recipe').click()
         self.assertEqual(
             extract_text(find_tags_by_class(browser.contents, 'message')[2]),

=== added file 'lib/lp/code/help/related-recipes.html'
--- lib/lp/code/help/related-recipes.html	1970-01-01 00:00:00 +0000
+++ lib/lp/code/help/related-recipes.html	2010-11-05 19:52:03 +0000
@@ -0,0 +1,38 @@
+<html>
+  <head>
+    <title>Related source package recipes</title>
+    <link rel="stylesheet" type="text/css"
+          href="/+icing/yui/cssreset/reset.css" />
+    <link rel="stylesheet" type="text/css"
+          href="/+icing/yui/cssfonts/fonts.css" />
+    <link rel="stylesheet" type="text/css"
+          href="/+icing/yui/cssbase/base.css" />
+  </head>
+  <body>
+    <h1>Related source package recipes</h1>
+
+    <p>
+      You can ask Launchpad to make an automatic daily build
+      of the code in this branch and place the resultant package(s)
+      in your chosen PPA. (<a href="https://help.launchpad.net/Packaging/SourceBuilds"; target="_blank">Read more</a>)
+    </p>
+
+    <p>A "recipe" is a description of the steps Launchpad's package builder
+      should take to construct a source package from a set of Bazaar branches
+      that you specify.
+    </p>
+
+    <p>
+      Using a recipe, you tell Launchpad:
+    </p>
+    <ul class="bulleted">
+      <li>in which branch to find the code</li>
+      <li>where to find the packaging information &mdash; e.g. in a separate
+      branch or in an existing Ubuntu package branch</li>
+      <li>the correct package version (so users will still be able to upgrade
+      to the stable version of the distro once it gets released)</li>
+      <li>any modifications necessary to make the source build properly</li>
+    </ul>
+
+  </body>
+</html>

=== modified file 'lib/lp/code/interfaces/sourcepackagerecipe.py'
--- lib/lp/code/interfaces/sourcepackagerecipe.py	2010-09-08 15:29:51 +0000
+++ lib/lp/code/interfaces/sourcepackagerecipe.py	2010-11-05 19:52:03 +0000
@@ -14,6 +14,7 @@
     'ISourcePackageRecipeData',
     'ISourcePackageRecipeSource',
     'MINIMAL_RECIPE_TEXT',
+    'recipes_enabled',
     ]
 
 
@@ -45,12 +46,14 @@
     TextLine,
     )
 
+from canonical.config import config
 from canonical.launchpad import _
 from canonical.launchpad.validators.name import name_validator
 from lp.code.interfaces.branch import IBranch
 from lp.registry.interfaces.distroseries import IDistroSeries
 from lp.registry.interfaces.pocket import PackagePublishingPocket
 from lp.registry.interfaces.role import IHasOwner
+from lp.services import features
 from lp.services.fields import (
     PersonChoice,
     PublicPersonChoice,
@@ -168,7 +171,8 @@
             " build a source package for"),
         readonly=False)
     build_daily = exported(Bool(
-        title=_("Build daily")))
+        title=_("Automatically build each day, if the source has changed"),
+        description=_("You can manually request a build at any time.")))
 
     name = exported(TextLine(
             title=_("Name"), required=True,
@@ -208,3 +212,13 @@
 
     def exists(owner, name):
         """Check to see if a recipe by the same name and owner exists."""
+
+
+def recipes_enabled():
+    """Return True if recipes are enabled."""
+    # Features win:
+    if features.getFeatureFlag(u'code.recipes_enabled'):
+        return True
+    if not config.build_from_branch.enabled:
+        return False
+    return True

=== modified file 'lib/lp/code/model/sourcepackagerecipe.py'
--- lib/lp/code/model/sourcepackagerecipe.py	2010-09-02 15:56:16 +0000
+++ lib/lp/code/model/sourcepackagerecipe.py	2010-11-05 19:52:03 +0000
@@ -46,6 +46,7 @@
     ISourcePackageRecipe,
     ISourcePackageRecipeData,
     ISourcePackageRecipeSource,
+    recipes_enabled,
     )
 from lp.code.interfaces.sourcepackagerecipebuild import (
     ISourcePackageRecipeBuildSource,
@@ -215,7 +216,7 @@
     def requestBuild(self, archive, requester, distroseries, pocket,
                      manual=False):
         """See `ISourcePackageRecipe`."""
-        if not config.build_from_branch.enabled:
+        if not recipes_enabled():
             raise ValueError('Source package recipe builds disabled.')
         if not archive.is_ppa:
             raise NonPPABuildRequest

=== modified file 'lib/lp/code/model/tests/test_sourcepackagerecipe.py'
--- lib/lp/code/model/tests/test_sourcepackagerecipe.py	2010-10-27 14:20:21 +0000
+++ lib/lp/code/model/tests/test_sourcepackagerecipe.py	2010-11-05 19:52:03 +0000
@@ -41,6 +41,7 @@
     ISourcePackageRecipe,
     ISourcePackageRecipeSource,
     MINIMAL_RECIPE_TEXT,
+    recipes_enabled,
     )
 from lp.code.interfaces.sourcepackagerecipebuild import (
     ISourcePackageRecipeBuild,
@@ -65,10 +66,12 @@
     )
 from lp.testing import (
     ANONYMOUS,
+    feature_flags,
     launchpadlib_for,
     login,
     login_person,
     person_logged_in,
+    set_feature_flag,
     TestCaseWithFactory,
     ws_object,
     )
@@ -79,6 +82,10 @@
 
     layer = DatabaseFunctionalLayer
 
+    def setUp(self):
+        super(TestSourcePackageRecipe, self).setUp()
+        self.useContext(feature_flags())
+
     def test_implements_interface(self):
         """SourcePackageRecipe implements ISourcePackageRecipe."""
         recipe = self.factory.makeSourcePackageRecipe()
@@ -347,6 +354,15 @@
             ValueError, recipe.requestBuild, ppa, ppa.owner, distroseries,
             PackagePublishingPocket.RELEASE)
 
+    def test_recipes_enabled_config(self):
+        self.pushConfig('build_from_branch', enabled=False)
+        self.assertFalse(recipes_enabled())
+
+    def test_recipes_enabled_flag(self):
+        self.pushConfig('build_from_branch', enabled=False)
+        set_feature_flag(u'code.recipes_enabled', u'on')
+        self.assertTrue(recipes_enabled())
+
     def test_requestBuildRejectsOverQuota(self):
         """Build requests that exceed quota raise an exception."""
         requester = self.factory.makePerson(name='requester')

=== modified file 'lib/lp/code/templates/branch-index.pt'
--- lib/lp/code/templates/branch-index.pt	2010-10-19 01:24:36 +0000
+++ lib/lp/code/templates/branch-index.pt	2010-11-05 19:52:03 +0000
@@ -111,7 +111,7 @@
            replace="structure context/@@++branch-pending-merges" />
       <tal:branch-recipes
            replace="structure context/@@++branch-recipes"
-           condition="modules/canonical.config/config/build_from_branch/enabled"
+           condition="modules/lp.code.interfaces.sourcepackagerecipe/recipes_enabled"
       />
       <tal:related-bugs-specs
            replace="structure context/@@++branch-related-bugs-specs" />

=== modified file 'lib/lp/code/templates/branch-recipes.pt'
--- lib/lp/code/templates/branch-recipes.pt	2010-07-21 13:22:25 +0000
+++ lib/lp/code/templates/branch-recipes.pt	2010-11-05 19:52:03 +0000
@@ -15,6 +15,9 @@
       </a>
       using this branch.
 
+      <a href="/+help/related-recipes.html" target="help" class="sprite maybe">
+        &nbsp;<span class="invisible-link">Tag help</span>
+      </a>
     </div>
 
     <span

=== added file 'lib/lp/code/templates/sourcepackagerecipe-new.pt'
--- lib/lp/code/templates/sourcepackagerecipe-new.pt	1970-01-01 00:00:00 +0000
+++ lib/lp/code/templates/sourcepackagerecipe-new.pt	2010-11-05 19:52:03 +0000
@@ -0,0 +1,29 @@
+<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">
+  <body>
+    <div metal:fill-slot="main">
+
+      <div>
+        <p>A "recipe" is a description of the steps bzr-builder should take to
+        construct a source package from the various bzr branches. Its format
+        specifies:</p>
+        <ul class="bulleted">
+          <li>where to use the code from (trunk branch, beta branch, etc.), where to get the packaging from (separate branch? ubuntu branch?)</li>
+          <li>the correct package version (so users will still be able to upgrade to the stable version of the distro once it gets released)</li>
+          <li>what to modify to make the source build properly</li>
+        </ul>
+
+        <p>We strongly recommend that you test your recipe locally first.
+        <a href="https://help.launchpad.net/Packaging/SourceBuilds/GettingStarted";>Read more...</a></p>
+
+      </div>
+
+      <div metal:use-macro="context/@@launchpad_form/form" />
+    </div>
+  </body>
+</html>

=== modified file 'lib/lp/coop/answersbugs/model.py'
--- lib/lp/coop/answersbugs/model.py	2010-10-03 15:30:06 +0000
+++ lib/lp/coop/answersbugs/model.py	2010-11-05 19:52:03 +0000
@@ -13,7 +13,7 @@
 from zope.interface import implements
 
 from canonical.database.sqlbase import SQLBase
-from canonical.launchpad.interfaces import IQuestionBug
+from lp.coop.answersbugs.interfaces import IQuestionBug
 
 
 class QuestionBug(SQLBase):

=== modified file 'lib/lp/registry/interfaces/distroseries.py'
--- lib/lp/registry/interfaces/distroseries.py	2010-11-02 06:21:58 +0000
+++ lib/lp/registry/interfaces/distroseries.py	2010-11-05 19:52:03 +0000
@@ -810,7 +810,6 @@
         description=copy_field(description, required=False),
         version=copy_field(version, required=False),
         distribution=copy_field(distribution, required=False),
-        status=copy_field(status, required=False),
         architectures=List(
             title=_("The list of architectures to copy to the derived "
             "distroseries."), value_type=TextLine(),
@@ -827,7 +826,7 @@
     @call_with(user=REQUEST_USER)
     @export_write_operation()
     def deriveDistroSeries(user, name, displayname, title, summary,
-                           description, version, distribution, status,
+                           description, version, distribution,
                            architectures, packagesets, rebuild):
         """Derive a distroseries from this one.
 
@@ -851,9 +850,6 @@
         :param distribution: The distribution the derived series will
             belong to. If it isn't specified this distroseries'
             distribution is used.
-        :param status: The status the new distroseries will be created
-            in. If the distroseries isn't specified, this parameter will
-            be ignored. Defaults to FROZEN.
         :param architectures: The architectures to copy to the derived
             series. If not specified, all of the architectures are copied.
         :param packagesets: The packagesets to copy to the derived series.

=== modified file 'lib/lp/registry/interfaces/person.py'
--- lib/lp/registry/interfaces/person.py	2010-11-04 19:59:02 +0000
+++ lib/lp/registry/interfaces/person.py	2010-11-05 19:52:03 +0000
@@ -1077,8 +1077,11 @@
         since it inherits from `IPerson`) is a member of himself
         (i.e. `person1.inTeam(person1)`).
 
-        :param team: An object providing `IPerson`, the name of a
-            team, or `None` (in which case `False` is returned).
+        :param team: One of an object providing `IPerson`, the string name of a
+            team or `None`. If a string was supplied the team is looked up.
+        :return: A bool with the result of the membership lookup. When looking
+            up the team from a string finds nothing or team was `None` then 
+            `False` is returned.
         """
 
     def clearInTeamCache():

=== modified file 'lib/lp/registry/model/distroseries.py'
--- lib/lp/registry/model/distroseries.py	2010-11-03 17:47:52 +0000
+++ lib/lp/registry/model/distroseries.py	2010-11-05 19:52:03 +0000
@@ -1890,8 +1890,7 @@
     def deriveDistroSeries(self, user, name, distribution=None,
                            displayname=None, title=None, summary=None,
                            description=None, version=None,
-                           status=SeriesStatus.FROZEN, architectures=(),
-                           packagesets=(), rebuild=False):
+                           architectures=(), packagesets=(), rebuild=False):
         """See `IDistroSeries`."""
         # XXX StevenK bug=643369 This should be in the security adapter
         # This should be allowed if the user is a driver for self.parent
@@ -1925,7 +1924,6 @@
                 name=name, displayname=displayname, title=title,
                 summary=summary, description=description,
                 version=version, parent_series=self, owner=user)
-            child.status = status
             IStore(self).add(child)
         else:
             if child.parent_series is not self:

=== modified file 'lib/lp/registry/model/person.py'
--- lib/lp/registry/model/person.py	2010-11-04 19:59:02 +0000
+++ lib/lp/registry/model/person.py	2010-11-05 19:52:03 +0000
@@ -1191,6 +1191,9 @@
         # Translate the team name to an ITeam if we were passed a team.
         if isinstance(team, (str, unicode)):
             team = PersonSet().getByName(team)
+            if team is None:
+                # No team, no membership.
+                return False
 
         if self.id == team.id:
             # A team is always a member of itself.

=== modified file 'lib/lp/registry/stories/webservice/xx-derivedistroseries.txt'
--- lib/lp/registry/stories/webservice/xx-derivedistroseries.txt	2010-10-28 19:46:28 +0000
+++ lib/lp/registry/stories/webservice/xx-derivedistroseries.txt	2010-11-05 19:52:03 +0000
@@ -13,9 +13,9 @@
     >>> soyuz = factory.makeTeam(name='soyuz-team')
     >>> parent = factory.makeDistroSeries()
     >>> child = factory.makeDistroSeries(name='child1', parent_series=parent)
-    >>> second_child = factory.makeDistroSeries(name='child2',
-    ...     parent_series=parent)
     >>> other = factory.makeDistroSeries()
+    >>> distribution = factory.makeDistribution(name='deribuntu', owner=soyuz)
+    >>> version = "%s.0" % factory.getUniqueInteger()
     >>> logout()
     >>> from canonical.launchpad.testing.pages import webservice_for_person
     >>> from canonical.launchpad.webapp.interfaces import OAuthPermission
@@ -60,10 +60,13 @@
 
 If we call it with all of the arguments, it also works.
 
-    >>> child_name = second_child.name
+    >>> deribuntu = webservice.get('/deribuntu').jsonBody()
+    >>> text = 'The Second Child'
     >>> derived = soyuz_webservice.named_post(
     ...     series['self_link'], 'deriveDistroSeries', {},
-    ...     name=child_name, architectures=('i386',),
+    ...     name='child2', displayname=text, title=text, summary=text,
+    ...     description=text, version=version,
+    ...     distribution=deribuntu['self_link'], architectures=('i386',),
     ...     packagesets=('test1',), rebuild=False)
     >>> print derived
     HTTP/1.1 200 Ok

=== modified file 'lib/lp/registry/tests/test_person.py'
--- lib/lp/registry/tests/test_person.py	2010-11-03 22:37:55 +0000
+++ lib/lp/registry/tests/test_person.py	2010-11-05 19:52:03 +0000
@@ -223,6 +223,13 @@
             {},
             removeSecurityProxy(self.user)._inTeam_cache)
 
+    def test_inTeam_person_string_missing_team(self):
+        # If a check against a string is done, the team lookup is implicit:
+        # treat a missing team as an empty team so that any pages that choose
+        # to do this don't blow up unnecessarily. Similarly feature flags 
+        # team: scopes depend on this.
+        self.assertFalse(self.user.inTeam('does-not-exist'))
+
 
 class TestPerson(TestCaseWithFactory):
 

=== modified file 'lib/lp/scripts/garbo.py'
--- lib/lp/scripts/garbo.py	2010-10-17 22:51:50 +0000
+++ lib/lp/scripts/garbo.py	2010-11-05 19:52:03 +0000
@@ -43,8 +43,8 @@
     )
 from canonical.launchpad.database.oauth import OAuthNonce
 from canonical.launchpad.database.openidconsumer import OpenIDConsumerNonce
-from canonical.launchpad.interfaces import IMasterStore
 from canonical.launchpad.interfaces.emailaddress import EmailAddressStatus
+from canonical.launchpad.interfaces.lpstorm import IMasterStore
 from canonical.launchpad.utilities.looptuner import TunableLoop
 from canonical.launchpad.webapp.interfaces import (
     IStoreSelector,

=== modified file 'lib/lp/services/features/tests/test_webapp.py'
--- lib/lp/services/features/tests/test_webapp.py	2010-09-13 00:55:15 +0000
+++ lib/lp/services/features/tests/test_webapp.py	2010-11-05 19:52:03 +0000
@@ -7,7 +7,11 @@
 
 from canonical.testing import layers
 from lp.services.features import webapp
-from lp.testing import TestCase
+from lp.testing import (
+    login_as,
+    TestCase,
+    TestCaseWithFactory,
+    )
 from canonical.launchpad.webapp.servers import LaunchpadTestRequest
 
 
@@ -38,4 +42,23 @@
         scopes = webapp.ScopesFromRequest(request)
         self.assertTrue(scopes.lookup('pageid:'))
         self.assertFalse(scopes.lookup('pageid:foo'))
-        self.assertFalse(scopes.lookup('pageid:foo'))
+        self.assertFalse(scopes.lookup('pageid:foo:bar'))
+
+
+class TestDBScopes(TestCaseWithFactory):
+
+    layer = layers.DatabaseFunctionalLayer
+
+    def test_team_scope_outside_team(self):
+        request = LaunchpadTestRequest()
+        scopes = webapp.ScopesFromRequest(request)
+        self.factory.loginAsAnyone()
+        self.assertFalse(scopes.lookup('team:nonexistent'))
+
+    def test_team_scope_in_team(self):
+        request = LaunchpadTestRequest()
+        scopes = webapp.ScopesFromRequest(request)
+        member = self.factory.makePerson()
+        team = self.factory.makeTeam(members=[member])
+        login_as(member)
+        self.assertTrue(scopes.lookup('team:%s' % team.name))

=== modified file 'lib/lp/services/features/webapp.py'
--- lib/lp/services/features/webapp.py	2010-10-25 17:29:07 +0000
+++ lib/lp/services/features/webapp.py	2010-11-05 19:52:03 +0000
@@ -7,7 +7,10 @@
 
 __metaclass__ = type
 
+from zope.component import getUtility
+
 import canonical.config
+from canonical.launchpad.webapp.interfaces import ILaunchBag
 from lp.services.features import per_thread
 from lp.services.features.flags import FeatureController
 from lp.services.features.rulesource import StormFeatureRuleSource
@@ -34,11 +37,16 @@
              - pageid:SomeType
              - pageid:SomeType:+view
              - pageid:SomeType:+view#subselector
+         - team:
+           This scope looks up a team. For instance
+             - team:launchpad-beta-users
         """
         if scope_name == 'default':
             return True
         if scope_name.startswith('pageid:'):
             return self._lookup_pageid(scope_name[len('pageid:'):])
+        if scope_name.startswith('team:'):
+            return self._lookup_team(scope_name[len('team:'):])
         parts = scope_name.split('.')
         if len(parts) == 2:
             if parts[0] == 'server':
@@ -65,6 +73,23 @@
                 return False
         return True
 
+    def _lookup_team(self, team_name):
+        """Lookup a team membership as a scope.
+
+        This will do a two queries, so we probably want to keep the number of
+        team based scopes in use to a small number. (Person.inTeam could be
+        fixed to reduce this to one query).
+
+        teamid scopes are written as 'team:' + the team name to match.
+
+        E.g. the scope 'team:launchpad-beta-users' will match members of
+        the team 'launchpad-beta-users'.
+        """
+        person = getUtility(ILaunchBag).user
+        if person is None:
+            return False
+        return person.inTeam(team_name)
+
     def _pageid_to_namespace(self, pageid):
         """Return a list of namespace elements for pageid."""
         # Normalise delimiters.

=== modified file 'lib/lp/services/fields/__init__.py'
--- lib/lp/services/fields/__init__.py	2010-10-03 15:30:06 +0000
+++ lib/lp/services/fields/__init__.py	2010-11-05 19:52:03 +0000
@@ -340,7 +340,7 @@
 
     def _get_schema(self):
         """Get the schema here to avoid circular imports."""
-        from canonical.launchpad.interfaces import IBug
+        from lp.bugs.interfaces.bug import IBug
         return IBug
 
     def _set_schema(self, schema):
@@ -809,7 +809,7 @@
 
 def is_public_person(person):
     """Return True if the person is public."""
-    from canonical.launchpad.interfaces import IPerson, PersonVisibility
+    from lp.registry.interfaces.person import IPerson, PersonVisibility
     if not IPerson.providedBy(person):
         return False
     return person.visibility == PersonVisibility.PUBLIC

=== modified file 'lib/lp/services/mail/incoming.py'
--- lib/lp/services/mail/incoming.py	2010-10-15 19:29:58 +0000
+++ lib/lp/services/mail/incoming.py	2010-11-05 19:52:03 +0000
@@ -27,15 +27,14 @@
     directlyProvides,
     )
 
-from canonical.launchpad.interfaces import (
-    AccountStatus,
+from canonical.launchpad.interfaces.account import AccountStatus
+from canonical.launchpad.interfaces.gpghandler import (
     GPGVerificationError,
     IGPGHandler,
-    ILibraryFileAliasSet,
-    IMailBox,
-    IPerson,
-    IWeaklyAuthenticatedPrincipal,
     )
+from canonical.launchpad.interfaces.librarian import ILibraryFileAliasSet
+from canonical.launchpad.interfaces.mail import IWeaklyAuthenticatedPrincipal
+from canonical.launchpad.interfaces.mailbox import IMailBox
 from canonical.launchpad.mail.commands import get_error_message
 from canonical.launchpad.mail.handlers import mail_handlers
 from canonical.launchpad.mailnotification import (
@@ -51,6 +50,7 @@
     )
 from canonical.launchpad.webapp.interfaces import IPlacelessAuthUtility
 from canonical.librarian.interfaces import UploadFailed
+from lp.registry.interfaces.person import IPerson
 from lp.services.mail.sendmail import do_paranoid_envelope_to_validation
 from lp.services.mail.signedmessage import signed_message_from_string
 

=== modified file 'lib/lp/services/mail/mailbox.py'
--- lib/lp/services/mail/mailbox.py	2010-10-03 15:30:06 +0000
+++ lib/lp/services/mail/mailbox.py	2010-11-05 19:52:03 +0000
@@ -10,7 +10,7 @@
 
 from zope.interface import implements
 
-from canonical.launchpad.interfaces import (
+from canonical.launchpad.interfaces.mailbox import (
     IMailBox,
     MailBoxError,
     )

=== modified file 'lib/lp/services/mail/notificationrecipientset.py'
--- lib/lp/services/mail/notificationrecipientset.py	2010-10-03 15:30:06 +0000
+++ lib/lp/services/mail/notificationrecipientset.py	2010-11-05 19:52:03 +0000
@@ -15,7 +15,7 @@
 from zope.security.proxy import isinstance as zope_isinstance
 
 from canonical.launchpad.helpers import emailPeople
-from canonical.launchpad.interfaces import (
+from canonical.launchpad.interfaces.launchpad import (
     INotificationRecipientSet,
     UnknownRecipientError,
     )

=== modified file 'lib/lp/services/mail/signedmessage.py'
--- lib/lp/services/mail/signedmessage.py	2010-10-03 15:30:06 +0000
+++ lib/lp/services/mail/signedmessage.py	2010-11-05 19:52:03 +0000
@@ -15,7 +15,7 @@
 
 from zope.interface import implements
 
-from canonical.launchpad.interfaces import ISignedMessage
+from canonical.launchpad.interfaces.mail import ISignedMessage
 
 
 clearsigned_re = re.compile(

=== modified file 'lib/lp/services/mailman/testing/sync.py'
--- lib/lp/services/mailman/testing/sync.py	2010-10-03 15:30:06 +0000
+++ lib/lp/services/mailman/testing/sync.py	2010-11-05 19:52:03 +0000
@@ -30,10 +30,8 @@
     login,
     logout,
     )
-from canonical.launchpad.interfaces import (
-    IEmailAddressSet,
-    IPersonSet,
-    )
+from canonical.launchpad.interfaces.emailaddress import IEmailAddressSet
+from lp.registry.interfaces.person import IPersonSet
 
 
 class SyncDetails:

=== modified file 'lib/lp/services/scripts/base.py'
--- lib/lp/services/scripts/base.py	2010-11-01 12:56:23 +0000
+++ lib/lp/services/scripts/base.py	2010-11-05 19:52:03 +0000
@@ -16,7 +16,11 @@
 from optparse import OptionParser
 import os.path
 import sys
-from urllib2 import urlopen, HTTPError, URLError
+from urllib2 import (
+    HTTPError,
+    URLError,
+    urlopen,
+    )
 
 from contrib.glock import (
     GlobalLock,
@@ -28,7 +32,6 @@
 from canonical.config import config
 from canonical.database.sqlbase import ISOLATION_LEVEL_DEFAULT
 from canonical.launchpad import scripts
-from canonical.launchpad.interfaces import IScriptActivitySet
 from canonical.launchpad.scripts.logger import OopsHandler
 from canonical.launchpad.webapp.errorlog import globalErrorUtility
 from canonical.launchpad.webapp.interaction import (
@@ -36,6 +39,7 @@
     setupInteractionByEmail,
     )
 from canonical.lp import initZopeless
+from lp.services.scripts.interfaces.scriptactivity import IScriptActivitySet
 
 
 LOCK_PATH = "/var/lock/"

=== modified file 'lib/lp/services/scripts/model/scriptactivity.py'
--- lib/lp/services/scripts/model/scriptactivity.py	2010-10-03 15:30:06 +0000
+++ lib/lp/services/scripts/model/scriptactivity.py	2010-11-05 19:52:03 +0000
@@ -17,7 +17,7 @@
 
 from canonical.database.datetimecol import UtcDateTimeCol
 from canonical.database.sqlbase import SQLBase
-from canonical.launchpad.interfaces import (
+from lp.services.scripts.interfaces.scriptactivity import (
     IScriptActivity,
     IScriptActivitySet,
     )

=== modified file 'lib/lp/services/worlddata/model/language.py'
--- lib/lp/services/worlddata/model/language.py	2010-10-03 15:30:06 +0000
+++ lib/lp/services/worlddata/model/language.py	2010-11-05 19:52:03 +0000
@@ -26,7 +26,7 @@
     SQLBase,
     sqlvalues,
     )
-from canonical.launchpad.interfaces import ISlaveStore
+from canonical.launchpad.interfaces.lpstorm import ISlaveStore
 from lp.app.errors import NotFoundError
 from lp.services.worlddata.interfaces.language import (
     ILanguage,

=== modified file 'lib/lp/testing/_webservice.py'
--- lib/lp/testing/_webservice.py	2010-10-03 15:30:06 +0000
+++ lib/lp/testing/_webservice.py	2010-11-05 19:52:03 +0000
@@ -26,13 +26,11 @@
 from zope.component import getUtility
 import zope.testing.cleanup
 
-from canonical.launchpad.interfaces import (
-    IOAuthConsumerSet,
-    IPersonSet,
-    )
+from canonical.launchpad.interfaces.oauth import IOAuthConsumerSet
 from canonical.launchpad.webapp.adapter import get_request_statements
 from canonical.launchpad.webapp.interaction import ANONYMOUS
 from canonical.launchpad.webapp.interfaces import OAuthPermission
+from lp.registry.interfaces.person import IPersonSet
 from lp.testing._login import (
     login,
     logout,

=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py	2010-11-04 19:59:02 +0000
+++ lib/lp/testing/factory.py	2010-11-05 19:52:03 +0000
@@ -68,10 +68,6 @@
     Message,
     MessageChunk,
     )
-from canonical.launchpad.interfaces import (
-    IMasterStore,
-    IStore,
-    )
 from canonical.launchpad.interfaces.account import (
     AccountCreationRationale,
     AccountStatus,
@@ -84,6 +80,10 @@
 from canonical.launchpad.interfaces.gpghandler import IGPGHandler
 from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
 from canonical.launchpad.interfaces.librarian import ILibraryFileAliasSet
+from canonical.launchpad.interfaces.lpstorm import (
+    IMasterStore,
+    IStore,
+    )
 from canonical.launchpad.interfaces.oauth import IOAuthConsumerSet
 from canonical.launchpad.interfaces.temporaryblobstorage import (
     ITemporaryStorageManager,
@@ -112,6 +112,10 @@
     IBugTrackerSet,
     )
 from lp.bugs.interfaces.bugwatch import IBugWatchSet
+from lp.bugs.interfaces.cve import (
+    CveStatus,
+    ICveSet,
+    )
 from lp.buildmaster.enums import (
     BuildFarmJobType,
     BuildStatus,
@@ -1633,7 +1637,8 @@
 
     def makeSpecification(self, product=None, title=None, distribution=None,
                           name=None, summary=None, owner=None,
-                          status=SpecificationDefinitionStatus.NEW):
+                          status=SpecificationDefinitionStatus.NEW,
+                          implementation_status=None):
         """Create and return a new, arbitrary Blueprint.
 
         :param product: The product to make the blueprint on.  If one is
@@ -1649,7 +1654,7 @@
             title = self.getUniqueString('title')
         if owner is None:
             owner = self.makePerson()
-        return getUtility(ISpecificationSet).new(
+        spec = getUtility(ISpecificationSet).new(
             name=name,
             title=title,
             specurl=None,
@@ -1658,6 +1663,11 @@
             owner=owner,
             product=product,
             distribution=distribution)
+        if implementation_status is not None:
+            naked_spec = removeSecurityProxy(spec)
+            naked_spec.implementation_status = implementation_status
+            naked_spec.updateLifecycleStatus(owner)
+        return spec
 
     def makeQuestion(self, target=None, title=None):
         """Create and return a new, arbitrary Question.
@@ -3251,6 +3261,13 @@
             consumer, reviewed_by=owner, access_level=access_level)
         return request_token.createAccessToken()
 
+    def makeCVE(self, sequence, description=None,
+                cvestate=CveStatus.CANDIDATE):
+        """Create a new CVE record."""
+        if description is None:
+            description = self.getUniqueString()
+        return getUtility(ICveSet).new(sequence, description, cvestate)
+
 
 # Some factory methods return simple Python types. We don't add
 # security wrappers for them, as well as for objects created by

=== modified file 'lib/lp/testing/mail.py'
--- lib/lp/testing/mail.py	2010-10-03 15:30:06 +0000
+++ lib/lp/testing/mail.py	2010-11-05 19:52:03 +0000
@@ -10,7 +10,7 @@
 
 from zope.component import getUtility
 
-from canonical.launchpad.interfaces import IMailBox
+from canonical.launchpad.interfaces.mailbox import IMailBox
 from canonical.launchpad.mail import (
     get_msgid,
     MailController,

=== modified file 'lib/lp/testing/tests/test_factory.py'
--- lib/lp/testing/tests/test_factory.py	2010-10-25 19:11:46 +0000
+++ lib/lp/testing/tests/test_factory.py	2010-11-05 19:52:03 +0000
@@ -17,6 +17,10 @@
     DatabaseFunctionalLayer,
     LaunchpadZopelessLayer,
     )
+from lp.bugs.interfaces.cve import (
+    CveStatus,
+    ICve,
+    )
 from lp.buildmaster.enums import BuildStatus
 from lp.code.enums import (
     BranchType,
@@ -492,6 +496,24 @@
         ssp = self.factory.makeSuiteSourcePackage()
         self.assertThat(ssp, ProvidesAndIsProxied(ISuiteSourcePackage))
 
+    # makeCVE
+    def test_makeCVE_returns_cve(self):
+        cve = self.factory.makeCVE(sequence='2000-1234')
+        self.assertThat(cve, ProvidesAndIsProxied(ICve))
+
+    def test_makeCVE_uses_sequence(self):
+        cve = self.factory.makeCVE(sequence='2000-1234')
+        self.assertEqual('2000-1234', cve.sequence)
+
+    def test_makeCVE_uses_description(self):
+        cve = self.factory.makeCVE(sequence='2000-1234', description='foo')
+        self.assertEqual('foo', cve.description)
+
+    def test_makeCVE_uses_cve_status(self):
+        cve = self.factory.makeCVE(
+            sequence='2000-1234', cvestate=CveStatus.DEPRECATED)
+        self.assertEqual(CveStatus.DEPRECATED, cve.status)
+
 
 class TestFactoryWithLibrarian(TestCaseWithFactory):
 

=== modified file 'utilities/migrater/deglob.py'
--- utilities/migrater/deglob.py	2010-10-20 20:45:23 +0000
+++ utilities/migrater/deglob.py	2010-11-05 19:52:03 +0000
@@ -5,6 +5,9 @@
 #find true path
 #replace all occurences.
 
+import os
+import sys
+
 from find import find_matches
 
 
@@ -228,10 +231,8 @@
             print "    %(lineno)4s: %(text)s" % line
 
 
-def update_multi_python_globs_to_interfaces():
-    root = 'lib'
-    types = r'tests'
-    pattern = r'from canonical\.launchpad\.interfaces import'
+def update_multi_python_globs_to_interfaces(root='lib', types='tests'):
+    pattern=r'from canonical\.launchpad\.interfaces import'
     substitution = True
     for summary in find_matches(
         root, types, pattern, substitution=substitution,
@@ -241,9 +242,8 @@
             print "    %(lineno)4s: %(text)s" % line
 
 
-def update_python_globs_to_interfaces():
-    root = 'lib'
-    types = r'tests'
+def update_python_globs_to_interfaces(root='lib', types='tests'):
+    update_multi_python_globs_to_interfaces(root=root, types=types)
     globs = r'from \bcanonical\.launchpad\.interfaces import (\w+)$'
     interfaces = get_interfaces(types=types, globs=globs)
     interface_modules = get_interface_modules(interfaces)
@@ -259,8 +259,12 @@
 
 
 def main():
-    update_multi_python_globs_to_interfaces()
-    update_python_globs_to_interfaces()
+    if len(sys.argv) != 3:
+        print 'Usage: %s root_path file_test', os.path.basename(sys.argv[0])
+        sys.exit(1)
+    root = sys.argv[1]
+    types = sys.argv[2]
+    update_python_globs_to_interfaces(root, types)
 
 
 if __name__ == '__main__':


Follow ups