← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~salgado/launchpad/expose-blueprints into lp:launchpad

 

Guilherme Salgado has proposed merging lp:~salgado/launchpad/expose-blueprints into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  #146389 api for blueprint tracker
  https://bugs.launchpad.net/bugs/146389


This branch exposes ISpecification attributes on the 'devel' version of the webservice.

I left out some controversial fields (i.e. .productseries, .distroseries) and exported .target as read-only for simplicity and because this diff is already too big.

Most of the changes here come from https://code.launchpad.net/~james-w/launchpad/expose-blueprints/+merge/30026; I just had to solve conflicts, clean some things up and unexport the controversial fields.  The diff is quite long but it's mostly mechanical changes and tests.
-- 
https://code.launchpad.net/~salgado/launchpad/expose-blueprints/+merge/41898
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~salgado/launchpad/expose-blueprints into lp:launchpad.
=== modified file 'lib/canonical/launchpad/interfaces/_schema_circular_imports.py'
--- lib/canonical/launchpad/interfaces/_schema_circular_imports.py	2010-11-17 22:18:34 +0000
+++ lib/canonical/launchpad/interfaces/_schema_circular_imports.py	2010-11-25 19:32:28 +0000
@@ -34,6 +34,10 @@
     )
 from lp.blueprints.interfaces.specification import ISpecification
 from lp.blueprints.interfaces.specificationbranch import ISpecificationBranch
+from lp.blueprints.interfaces.specificationtarget import (
+    IHasSpecifications,
+    ISpecificationTarget,
+    )
 from lp.bugs.interfaces.bug import (
     IBug,
     IFrontPageBugAddForm,
@@ -516,3 +520,13 @@
 
 # IProductSeries
 patch_reference_property(IProductSeries, 'product', IProduct)
+
+# ISpecificationTarget
+patch_entry_return_type(
+    ISpecificationTarget, 'getSpecification', ISpecification)
+
+# IHasSpecifications
+patch_collection_property(
+    IHasSpecifications, 'all_specifications', ISpecification)
+patch_collection_property(
+    IHasSpecifications, 'valid_specifications', ISpecification)

=== modified file 'lib/lp/app/doc/tales.txt'
--- lib/lp/app/doc/tales.txt	2010-10-18 22:24:59 +0000
+++ lib/lp/app/doc/tales.txt	2010-11-25 19:32:28 +0000
@@ -673,8 +673,11 @@
 Blueprints
 ..........
 
+    >>> from lp.blueprints.interfaces.specification import (
+    ...     SpecificationPriority)
     >>> login('test@xxxxxxxxxxxxx')
-    >>> specification = factory.makeSpecification()
+    >>> specification = factory.makeSpecification(
+    ...     priority=SpecificationPriority.UNDEFINED)
     >>> test_tales("specification/fmt:link", specification=specification)
     u'<a...class="sprite blueprint-undefined">...</a>'
 
@@ -682,7 +685,8 @@
 Blueprint branches
 ..................
 
-    >>> specification = factory.makeSpecification()
+    >>> specification = factory.makeSpecification(
+    ...     priority=SpecificationPriority.UNDEFINED)
     >>> branch = factory.makeAnyBranch()
     >>> specification_branch = specification.linkBranch(branch, branch.owner)
     >>> test_tales("specification_branch/fmt:link",

=== modified file 'lib/lp/blueprints/interfaces/specification.py'
--- lib/lp/blueprints/interfaces/specification.py	2010-11-23 20:19:24 +0000
+++ lib/lp/blueprints/interfaces/specification.py	2010-11-25 19:32:28 +0000
@@ -19,7 +19,12 @@
     ]
 
 
-from lazr.restful.declarations import export_as_webservice_entry
+from lazr.restful.declarations import (
+    exported,
+    export_as_webservice_entry,
+    )
+from lazr.restful.fields import ReferenceChoice
+
 from zope.component import getUtility
 from zope.interface import (
     Attribute,
@@ -44,9 +49,13 @@
     SpecificationLifecycleStatus,
     SpecificationPriority,
     )
-from lp.blueprints.interfaces.specificationtarget import IHasSpecifications
+from lp.blueprints.interfaces.specificationtarget import (
+    IHasSpecifications,
+    ISpecificationTarget,
+    )
 from lp.blueprints.interfaces.sprint import ISprint
 from lp.code.interfaces.branchlink import IHasLinkedBranches
+from lp.registry.interfaces.milestone import IMilestone
 from lp.registry.interfaces.projectgroup import IProjectGroup
 from lp.registry.interfaces.role import IHasOwner
 from lp.services.fields import (
@@ -119,48 +128,66 @@
 class INewSpecification(Interface):
     """A schema for a new specification."""
 
-    name = SpecNameField(
-        title=_('Name'), required=True, readonly=False,
-        description=_(
-            "May contain lower-case letters, numbers, and dashes. "
-            "It will be used in the specification url. "
-            "Examples: mozilla-type-ahead-find, postgres-smart-serial."))
-    title = Title(
-        title=_('Title'), required=True, description=_(
-            "Describe the feature as clearly as possible in up to 70 "
-            "characters. This title is displayed in every feature "
-            "list or report."))
-    specurl = SpecURLField(
-        title=_('Specification URL'), required=False,
-        description=_(
-            "The URL of the specification. This is usually a wiki page."),
-        constraint=valid_webref)
-    summary = Summary(
-        title=_('Summary'), required=True, description=_(
-            "A single-paragraph description of the feature. "
-            "This will also be displayed in most feature listings."))
-    definition_status = Choice(
-        title=_('Definition Status'),
-        vocabulary=SpecificationDefinitionStatus,
-        default=SpecificationDefinitionStatus.NEW,
-        description=_(
-            "The current status of the process to define the "
-            "feature and get approval for the implementation plan."))
-    assignee = PublicPersonChoice(
-        title=_('Assignee'), required=False,
-        description=_("The person responsible for implementing the feature."),
-        vocabulary='ValidPersonOrTeam')
-    drafter = PublicPersonChoice(
-        title=_('Drafter'), required=False,
-        description=_(
-                "The person responsible for drafting the specification."),
-        vocabulary='ValidPersonOrTeam')
-    approver = PublicPersonChoice(
-        title=_('Approver'), required=False,
-        description=_(
-            "The person responsible for approving the specification, "
-            "and for reviewing the code when it's ready to be landed."),
-        vocabulary='ValidPersonOrTeam')
+    name = exported(
+        SpecNameField(
+            title=_('Name'), required=True, readonly=False,
+            description=_(
+                "May contain lower-case letters, numbers, and dashes. "
+                "It will be used in the specification url. "
+                "Examples: mozilla-type-ahead-find, postgres-smart-serial.")),
+        ('devel', dict(exported=True)), exported=False)
+    title = exported(
+        Title(
+            title=_('Title'), required=True, description=_(
+                "Describe the feature as clearly as possible in up to 70 "
+                "characters. This title is displayed in every feature "
+                "list or report.")),
+        ('devel', dict(exported=True)), exported=False)
+    specurl = exported(
+        SpecURLField(
+            title=_('Specification URL'), required=False,
+            description=_(
+                "The URL of the specification. This is usually a wiki page."),
+            constraint=valid_webref),
+        ('devel', dict(exported=True, exported_as='specification_url')),
+        exported=False)
+    summary = exported(
+        Summary(
+            title=_('Summary'), required=True, description=_(
+                "A single-paragraph description of the feature. "
+                "This will also be displayed in most feature listings.")),
+        ('devel', dict(exported=True)), exported=False)
+    definition_status = exported(
+        Choice(
+            title=_('Definition Status'),
+            vocabulary=SpecificationDefinitionStatus,
+            default=SpecificationDefinitionStatus.NEW,
+            description=_(
+                "The current status of the process to define the "
+                "feature and get approval for the implementation plan.")),
+        ('devel', dict(exported=True)), exported=False)
+    assignee = exported(
+        PublicPersonChoice(
+            title=_('Assignee'), required=False,
+            description=_(
+                "The person responsible for implementing the feature."),
+            vocabulary='ValidPersonOrTeam'),
+        ('devel', dict(exported=True)), exported=False)
+    drafter = exported(
+        PublicPersonChoice(
+            title=_('Drafter'), required=False,
+            description=_(
+                    "The person responsible for drafting the specification."),
+                vocabulary='ValidPersonOrTeam'),
+        ('devel', dict(exported=True)), exported=False)
+    approver = exported(
+        PublicPersonChoice(
+            title=_('Approver'), required=False,
+            description=_(
+                "The person responsible for approving the specification, "
+                "and for reviewing the code when it's ready to be landed."),
+            vocabulary='ValidPersonOrTeam'),
+        ('devel', dict(exported=True)), exported=False)
 
 
 class INewSpecificationProjectTarget(Interface):
@@ -201,10 +228,15 @@
 
     Requires the user to specify a distribution or a product as a target.
     """
-    target = Choice(title=_("For"),
-                    description=_("The project for which this proposal is "
-                                  "being made."),
-                    required=True, vocabulary='DistributionOrProduct')
+    # Exported as readonly for simplicity, but could be exported as read-write
+    # using setTarget() as the mutator.
+    target = exported(
+        ReferenceChoice(
+            title=_('For'), required=True, vocabulary='DistributionOrProduct',
+            description=_(
+                "The project for which this proposal is being made."),
+            schema=ISpecificationTarget),
+        ('devel', dict(exported=True, readonly=True)), exported=False)
 
 
 class ISpecificationEditRestricted(Interface):
@@ -235,40 +267,54 @@
     #      referencing it.
     id = Int(title=_("Database ID"), required=True, readonly=True)
 
-    priority = Choice(
-        title=_('Priority'), vocabulary=SpecificationPriority,
-        default=SpecificationPriority.UNDEFINED, required=True)
-    datecreated = Datetime(
-        title=_('Date Created'), required=True, readonly=True)
-    owner = PublicPersonChoice(
-        title=_('Owner'), required=True, readonly=True,
-        vocabulary='ValidPersonOrTeam')
-    # target
+    priority = exported(
+        Choice(
+            title=_('Priority'), vocabulary=SpecificationPriority,
+            default=SpecificationPriority.UNDEFINED, required=True),
+        ('devel', dict(exported=True)), exported=False)
+    datecreated = exported(
+        Datetime(
+            title=_('Date Created'), required=True, readonly=True),
+        ('devel', dict(exported=True, exported_as='date_created')),
+        exported=False)
+    owner = exported(
+        PublicPersonChoice(
+            title=_('Owner'), required=True, readonly=True,
+            vocabulary='ValidPersonOrTeam'),
+        ('devel', dict(exported=True)), exported=False)
+
     product = Choice(title=_('Project'), required=False,
-        vocabulary='Product')
+                     vocabulary='Product')
     distribution = Choice(title=_('Distribution'), required=False,
-        vocabulary='Distribution')
+                          vocabulary='Distribution')
 
-    # series
-    productseries = Choice(title=_('Series Goal'), required=False,
+    productseries = Choice(
+        title=_('Series Goal'), required=False,
         vocabulary='FilteredProductSeries',
         description=_(
-            "Choose a series in which you would like to deliver "
-            "this feature. Selecting '(no value)' will clear the goal."))
-    distroseries = Choice(title=_('Series Goal'), required=False,
+             "Choose a series in which you would like to deliver "
+             "this feature. Selecting '(no value)' will clear the goal."))
+    distroseries = Choice(
+        title=_('Series Goal'), required=False,
         vocabulary='FilteredDistroSeries',
         description=_(
             "Choose a series in which you would like to deliver "
             "this feature. Selecting '(no value)' will clear the goal."))
 
     # milestone
-    milestone = Choice(
-        title=_('Milestone'), required=False, vocabulary='Milestone',
-        description=_(
-            "The milestone in which we would like this feature to be "
-            "delivered."))
+    milestone = exported(
+        ReferenceChoice(
+            title=_('Milestone'), required=False, vocabulary='Milestone',
+            description=_(
+                "The milestone in which we would like this feature to be "
+                "delivered."),
+            schema=IMilestone),
+        ('devel', dict(exported=True)), exported=False)
 
     # nomination to a series for release management
+    # XXX: It'd be nice to export goal as read-only, but it's tricky because
+    # users will need to be aware of goalstatus as what's returned by .goal
+    # may not be the accepted goal.
     goal = Attribute("The series for which this feature is a goal.")
     goalstatus = Choice(
         title=_('Goal Acceptance'), vocabulary=SpecificationGoalStatus,
@@ -283,10 +329,12 @@
     date_goal_decided = Attribute("The date the spec was approved "
         "or declined as a goal.")
 
-    whiteboard = Text(title=_('Status Whiteboard'), required=False,
-        description=_(
-            "Any notes on the status of this spec you would like to make. "
-            "Your changes will override the current text."))
+    whiteboard = exported(
+        Text(title=_('Status Whiteboard'), required=False,
+             description=_(
+                "Any notes on the status of this spec you would like to "
+                "make. Your changes will override the current text.")),
+        ('devel', dict(exported=True)), exported=False)
     direction_approved = Bool(title=_('Basic direction approved?'),
         required=False, default=False, description=_("Check this to "
         "indicate that the drafter and assignee have satisfied the "

=== modified file 'lib/lp/blueprints/interfaces/specificationtarget.py'
--- lib/lp/blueprints/interfaces/specificationtarget.py	2010-08-20 20:31:18 +0000
+++ lib/lp/blueprints/interfaces/specificationtarget.py	2010-11-25 19:32:28 +0000
@@ -17,6 +17,22 @@
     Attribute,
     Interface,
     )
+from zope.schema import TextLine
+
+from lazr.restful.declarations import (
+    exported,
+    export_as_webservice_entry,
+    export_read_operation,
+    operation_for_version,
+    operation_parameters,
+    operation_returns_entry,
+    )
+from lazr.restful.fields import (
+    CollectionField,
+    Reference,
+    )
+
+from canonical.launchpad import _
 
 
 class IHasSpecifications(Interface):
@@ -26,16 +42,30 @@
     associated with them, and you can use this interface to query those.
     """
 
-    all_specifications = Attribute(
-        'A list of all specifications, regardless of status or approval '
-        'or completion, for this object.')
+    all_specifications = exported(
+        CollectionField(
+            title=_("All specifications"),
+            value_type=Reference(schema=Interface),  # ISpecification, really.
+            readonly=True,
+            description=_(
+                'A list of all specifications, regardless of status or '
+                'approval or completion, for this object.')),
+        ('devel', dict(exported=True)), exported=False)
 
     has_any_specifications = Attribute(
         'A true or false indicator of whether or not this object has any '
         'specifications associated with it, regardless of their status.')
 
-    valid_specifications = Attribute(
-        'A list of all specifications that are not obsolete.')
+    valid_specifications = exported(
+        CollectionField(
+            title=_("Valid specifications"),
+            value_type=Reference(schema=Interface),  # ISpecification, really.
+            readonly=True,
+            description=_(
+                'All specifications that are not obsolete. When called from '
+                'an ISpecificationGoal it will also exclude the ones that '
+                'have not been accepted for that goal')),
+        ('devel', dict(exported=True)), exported=False)
 
     latest_specifications = Attribute(
         "The latest 5 specifications registered for this context.")
@@ -63,12 +93,18 @@
         """
 
 
-
 class ISpecificationTarget(IHasSpecifications):
     """An interface for the objects which actually have unique
     specifications directly attached to them.
     """
 
+    export_as_webservice_entry()
+
+    @operation_parameters(
+        name=TextLine(title=_('The name of the specification')))
+    @operation_returns_entry(Interface) # really ISpecification
+    @export_read_operation()
+    @operation_for_version('devel')
     def getSpecification(name):
         """Returns the specification with the given name, for this target,
         or None.

=== modified file 'lib/lp/blueprints/interfaces/webservice.py'
--- lib/lp/blueprints/interfaces/webservice.py	2010-11-09 16:25:22 +0000
+++ lib/lp/blueprints/interfaces/webservice.py	2010-11-25 19:32:28 +0000
@@ -16,6 +16,7 @@
 
 from lp.blueprints.interfaces.specification import ISpecification
 from lp.blueprints.interfaces.specificationbranch import ISpecificationBranch
+from lp.blueprints.interfaces.specificationtarget import ISpecificationTarget
 # XXX: JonathanLange 2010-11-09 bug=673083: Legacy work-around for circular
 # import bugs.  Break this up into a per-package thing.
 from canonical.launchpad.interfaces import _schema_circular_imports

=== modified file 'lib/lp/blueprints/stories/standalone/sprint-links.txt'
--- lib/lp/blueprints/stories/standalone/sprint-links.txt	2009-09-22 10:48:09 +0000
+++ lib/lp/blueprints/stories/standalone/sprint-links.txt	2010-11-25 19:32:28 +0000
@@ -13,10 +13,6 @@
   >>> browser.open('http://blueprints.launchpad.dev/firefox/+spec/canvas')
   >>> browser.isHtml
   True
-  >>> 'Accepted' in browser.contents # make sure the page is not polluted
-  False
-  >>> 'Proposed' in browser.contents # make sure the page is not polluted
-  False
 
 Then we are going to propose it for the meeting agenda:
 

=== added file 'lib/lp/blueprints/tests/test_implements.py'
--- lib/lp/blueprints/tests/test_implements.py	1970-01-01 00:00:00 +0000
+++ lib/lp/blueprints/tests/test_implements.py	2010-11-25 19:32:28 +0000
@@ -0,0 +1,61 @@
+# Copyright 2009 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Tests that various objects implement specification-related interfaces."""
+
+__metaclass__ = type
+
+from canonical.testing import DatabaseFunctionalLayer
+from lp.blueprints.interfaces.specificationtarget import (
+    IHasSpecifications, ISpecificationTarget)
+from lp.testing import TestCaseWithFactory
+
+
+class ImplementsIHasSpecificationsTests(TestCaseWithFactory):
+    """Test that various objects implement IHasSpecifications."""
+    layer = DatabaseFunctionalLayer
+
+    def test_product_implements_IHasSpecifications(self):
+        product = self.factory.makeProduct()
+        self.assertProvides(product, IHasSpecifications)
+
+    def test_distribution_implements_IHasSpecifications(self):
+        product = self.factory.makeProduct()
+        self.assertProvides(product, IHasSpecifications)
+
+    def test_projectgroup_implements_IHasSpecifications(self):
+        projectgroup = self.factory.makeProject()
+        self.assertProvides(projectgroup, IHasSpecifications)
+
+    def test_person_implements_IHasSpecifications(self):
+        person = self.factory.makePerson()
+        self.assertProvides(person, IHasSpecifications)
+
+    def test_productseries_implements_IHasSpecifications(self):
+        productseries = self.factory.makeProductSeries()
+        self.assertProvides(productseries, IHasSpecifications)
+
+    def test_distroseries_implements_IHasSpecifications(self):
+        distroseries = self.factory.makeDistroSeries()
+        self.assertProvides(distroseries, IHasSpecifications)
+
+
+class ImplementsISpecificationTargetTests(TestCaseWithFactory):
+    """Test that various objects implement ISpecificationTarget."""
+    layer = DatabaseFunctionalLayer
+
+    def test_product_implements_ISpecificationTarget(self):
+        product = self.factory.makeProduct()
+        self.assertProvides(product, ISpecificationTarget)
+
+    def test_distribution_implements_ISpecificationTarget(self):
+        product = self.factory.makeProduct()
+        self.assertProvides(product, ISpecificationTarget)
+
+    def test_productseries_implements_ISpecificationTarget(self):
+        productseries = self.factory.makeProductSeries()
+        self.assertProvides(productseries, ISpecificationTarget)
+
+    def test_distroseries_implements_ISpecificationTarget(self):
+        distroseries = self.factory.makeDistroSeries()
+        self.assertProvides(distroseries, ISpecificationTarget)

=== added file 'lib/lp/blueprints/tests/test_webservice.py'
--- lib/lp/blueprints/tests/test_webservice.py	1970-01-01 00:00:00 +0000
+++ lib/lp/blueprints/tests/test_webservice.py	2010-11-25 19:32:28 +0000
@@ -0,0 +1,410 @@
+# Copyright 2009 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Webservice unit tests related to Launchpad blueprints."""
+
+__metaclass__ = type
+
+from canonical.testing import DatabaseFunctionalLayer
+from canonical.launchpad.testing.pages import webservice_for_person
+from lp.blueprints.interfaces.specification import (
+    SpecificationDefinitionStatus,
+    SpecificationPriority)
+from lp.testing import (
+    launchpadlib_for, TestCaseWithFactory)
+
+
+class SpecificationWebserviceTestCase(TestCaseWithFactory):
+
+    def makeProduct(self):
+        return self.factory.makeProduct(name="fooix")
+
+    def makeDistribution(self):
+        return self.factory.makeDistribution(name="foobuntu")
+
+    def getLaunchpadlib(self):
+        user = self.factory.makePerson()
+        return launchpadlib_for("testing", user, version='devel')
+
+    def getSpecOnWebservice(self, spec_object):
+        launchpadlib = self.getLaunchpadlib()
+        if spec_object.product is not None:
+            pillar_name = spec_object.product.name
+        else:
+            pillar_name = spec_object.distribution.name
+        return launchpadlib.load(
+            str(launchpadlib._root_uri) + '/%s/+spec/%s'
+            % (pillar_name, spec_object.name))
+
+    def getPillarOnWebservice(self, pillar_obj):
+        launchpadlib = self.getLaunchpadlib()
+        return launchpadlib.load(
+            str(launchpadlib._root_uri) + '/' + pillar_obj.name)
+
+
+class SpecificationAttributeWebserviceTests(SpecificationWebserviceTestCase):
+    """Test accessing specification attributes over the webservice."""
+    layer = DatabaseFunctionalLayer
+
+    def makeSimpleSpecification(self):
+        self.name = "some-spec"
+        self.title = "some-title"
+        self.url = "http://example.org/some_url";
+        self.summary = "Some summary."
+        status = SpecificationDefinitionStatus.PENDINGAPPROVAL
+        self.definition_status = status.title
+        self.assignee_name = "james-w"
+        assignee = self.factory.makePerson(name=self.assignee_name)
+        self.drafter_name = "jml"
+        drafter = self.factory.makePerson(name=self.drafter_name)
+        self.approver_name = "bob"
+        approver = self.factory.makePerson(name=self.approver_name)
+        self.owner_name = "mary"
+        owner = self.factory.makePerson(name=self.owner_name)
+        priority = SpecificationPriority.HIGH
+        self.priority = priority.title
+        self.whiteboard = "Some whiteboard"
+        self.product = self.factory.makeProduct()
+        return self.factory.makeSpecification(
+            product=self.product, name=self.name,
+            title=self.title, specurl=self.url,
+            summary=self.summary,
+            status=status,
+            assignee=assignee, drafter=drafter, approver=approver,
+            priority=priority,
+            owner=owner, whiteboard=self.whiteboard)
+
+    def getSimpleSpecificationResponse(self):
+        self.spec_object = self.makeSimpleSpecification()
+        return self.getSpecOnWebservice(self.spec_object)
+
+    def test_representation_is_empty_on_1_dot_0(self):
+        # ISpecification is exposed on the 1.0 version so that they can be
+        # linked against branches, but none of its fields is exposed on that
+        # version as we expect it to undergo significant refactorings before
+        # it's ready for prime time.
+        spec = self.makeSimpleSpecification()
+        user = self.factory.makePerson()
+        webservice = webservice_for_person(user)
+        response = webservice.get(
+            '/%s/+spec/%s' % (spec.product.name, spec.name))
+        expected_keys = sorted(
+            [u'self_link', u'http_etag', u'resource_type_link'])
+        self.assertEqual(response.status, 200)
+        self.assertEqual(sorted(response.jsonBody().keys()), expected_keys)
+
+    def test_representation_contains_name(self):
+        spec = self.getSimpleSpecificationResponse()
+        self.assertEqual(self.name, spec.name)
+
+    def test_representation_contains_target(self):
+        spec = self.getSimpleSpecificationResponse()
+        self.assertEqual(self.product.name, spec.target.name)
+
+    def test_representation_contains_title(self):
+        spec = self.getSimpleSpecificationResponse()
+        self.assertEqual(self.title, spec.title)
+
+    def test_representation_contains_specification_url(self):
+        spec = self.getSimpleSpecificationResponse()
+        self.assertEqual(self.url, spec.specification_url)
+
+    def test_representation_contains_summary(self):
+        spec = self.getSimpleSpecificationResponse()
+        self.assertEqual(self.summary, spec.summary)
+
+    def test_representation_contains_definition_status(self):
+        spec = self.getSimpleSpecificationResponse()
+        self.assertEqual(
+            self.definition_status, spec.definition_status)
+
+    def test_representation_contains_assignee(self):
+        spec = self.getSimpleSpecificationResponse()
+        self.assertEqual(self.assignee_name, spec.assignee.name)
+
+    def test_representation_contains_drafter(self):
+        spec = self.getSimpleSpecificationResponse()
+        self.assertEqual(self.drafter_name, spec.drafter.name)
+
+    def test_representation_contains_approver(self):
+        spec = self.getSimpleSpecificationResponse()
+        self.assertEqual(self.approver_name, spec.approver.name)
+
+    def test_representation_contains_owner(self):
+        spec = self.getSimpleSpecificationResponse()
+        self.assertEqual(self.owner_name, spec.owner.name)
+
+    def test_representation_contains_priority(self):
+        spec = self.getSimpleSpecificationResponse()
+        self.assertEqual(self.priority, spec.priority)
+
+    def test_representation_contains_date_created(self):
+        spec = self.getSimpleSpecificationResponse()
+        self.assertEqual(self.spec_object.datecreated, spec.date_created)
+
+    def test_representation_contains_whiteboard(self):
+        spec = self.getSimpleSpecificationResponse()
+        self.assertEqual(self.whiteboard, spec.whiteboard)
+
+    def test_representation_contains_milestone(self):
+        product = self.makeProduct()
+        productseries = self.factory.makeProductSeries(product=product)
+        milestone = self.factory.makeMilestone(
+            name="1.0", product=product, productseries=productseries)
+        spec_object = self.factory.makeSpecification(
+            product=product, goal=productseries, milestone=milestone)
+        spec = self.getSpecOnWebservice(spec_object)
+        self.assertEqual("1.0", spec.milestone.name)
+
+
+class SpecificationTargetTests(SpecificationWebserviceTestCase):
+    """Tests for accessing specifications via their targets."""
+    layer = DatabaseFunctionalLayer
+
+    def test_get_specification_on_product(self):
+        product = self.makeProduct()
+        spec_object = self.factory.makeSpecification(
+            product=product, name="some-spec")
+        product_on_webservice = self.getPillarOnWebservice(product)
+        spec = product_on_webservice.getSpecification(name="some-spec")
+        self.assertEqual("some-spec", spec.name)
+        self.assertEqual("fooix", spec.target.name)
+
+    def test_get_specification_on_distribution(self):
+        distribution = self.makeDistribution()
+        spec_object = self.factory.makeSpecification(
+            distribution=distribution, name="some-spec")
+        distro_on_webservice = self.getPillarOnWebservice(distribution)
+        spec = distro_on_webservice.getSpecification(name="some-spec")
+        self.assertEqual("some-spec", spec.name)
+        self.assertEqual("foobuntu", spec.target.name)
+
+    def test_get_specification_on_productseries(self):
+        product = self.makeProduct()
+        productseries = self.factory.makeProductSeries(
+            product=product, name="fooix-dev")
+        spec_object = self.factory.makeSpecification(
+            product=product, name="some-spec", goal=productseries)
+        product_on_webservice = self.getPillarOnWebservice(product)
+        productseries_on_webservice = product_on_webservice.getSeries(
+            name="fooix-dev")
+        spec = productseries_on_webservice.getSpecification(name="some-spec")
+        self.assertEqual("some-spec", spec.name)
+        self.assertEqual("fooix", spec.target.name)
+
+    def test_get_specification_on_distroseries(self):
+        distribution = self.makeDistribution()
+        distroseries = self.factory.makeDistroSeries(
+            distribution=distribution, name="maudlin")
+        spec_object = self.factory.makeSpecification(
+            distribution=distribution, name="some-spec",
+            goal=distroseries)
+        distro_on_webservice = self.getPillarOnWebservice(distribution)
+        distroseries_on_webservice = distro_on_webservice.getSeries(
+            name_or_version="maudlin")
+        spec = distroseries_on_webservice.getSpecification(name="some-spec")
+        self.assertEqual("some-spec", spec.name)
+        self.assertEqual("foobuntu", spec.target.name)
+
+    def test_get_specification_not_found(self):
+        product = self.makeProduct()
+        product_on_webservice = self.getPillarOnWebservice(product)
+        spec = product_on_webservice.getSpecification(name="nonexistant")
+        self.assertEqual(None, spec)
+
+
+class IHasSpecificationsTests(SpecificationWebserviceTestCase):
+    """Tests for accessing IHasSpecifications methods over the webservice."""
+    layer = DatabaseFunctionalLayer
+
+    def assertNamesOfSpecificationsAre(self, expected_names, specifications):
+        names = [s.name for s in specifications]
+        self.assertEqual(sorted(expected_names), sorted(names))
+
+    def test_product_all_specifications(self):
+        product = self.makeProduct()
+        self.factory.makeSpecification(product=product, name="spec1")
+        self.factory.makeSpecification(product=product, name="spec2")
+        product_on_webservice = self.getPillarOnWebservice(product)
+        self.assertNamesOfSpecificationsAre(
+            ["spec1", "spec2"], product_on_webservice.all_specifications)
+
+    def test_product_valid_specifications(self):
+        product = self.makeProduct()
+        self.factory.makeSpecification(product=product, name="spec1")
+        self.factory.makeSpecification(
+            product=product, name="spec2",
+            status=SpecificationDefinitionStatus.OBSOLETE)
+        product_on_webservice = self.getPillarOnWebservice(product)
+        self.assertNamesOfSpecificationsAre(
+            ["spec1"], product_on_webservice.valid_specifications)
+
+    def test_distribution_all_specifications(self):
+        distribution = self.makeDistribution()
+        self.factory.makeSpecification(
+            distribution=distribution, name="spec1")
+        self.factory.makeSpecification(
+            distribution=distribution, name="spec2")
+        distro_on_webservice = self.getPillarOnWebservice(distribution)
+        self.assertNamesOfSpecificationsAre(
+            ["spec1", "spec2"], distro_on_webservice.all_specifications)
+
+    def test_distribution_valid_specifications(self):
+        distribution = self.makeDistribution()
+        self.factory.makeSpecification(
+            distribution=distribution, name="spec1")
+        self.factory.makeSpecification(
+            distribution=distribution, name="spec2",
+            status=SpecificationDefinitionStatus.OBSOLETE)
+        distro_on_webservice = self.getPillarOnWebservice(distribution)
+        self.assertNamesOfSpecificationsAre(
+            ["spec1"], distro_on_webservice.valid_specifications)
+
+    def test_distroseries_all_specifications(self):
+        distribution = self.makeDistribution()
+        distroseries = self.factory.makeDistroSeries(
+            name='maudlin', distribution=distribution)
+        self.factory.makeSpecification(
+            distribution=distribution, name="spec1",
+            goal=distroseries)
+        self.factory.makeSpecification(
+            distribution=distribution, name="spec2",
+            goal=distroseries)
+        self.factory.makeSpecification(
+            distribution=distribution, name="spec3")
+        distro_on_webservice = self.getPillarOnWebservice(distribution)
+        distroseries_on_webservice = distro_on_webservice.getSeries(
+            name_or_version="maudlin")
+        self.assertNamesOfSpecificationsAre(
+            ["spec1", "spec2"],
+            distroseries_on_webservice.all_specifications)
+
+    # XXX: salgado, 2010-11-25, bug=681432: Test disabled because
+    # DistroSeries.valid_specifications is broken.
+    def disabled_test_distroseries_valid_specifications(self):
+        distribution = self.makeDistribution()
+        distroseries = self.factory.makeDistroSeries(
+            name='maudlin', distribution=distribution)
+        self.factory.makeSpecification(
+            distribution=distribution, name="spec1",
+            goal=distroseries)
+        self.factory.makeSpecification(
+            distribution=distribution, name="spec2",
+            goal=distroseries)
+        self.factory.makeSpecification(
+            distribution=distribution, name="spec3",
+            goal=distroseries,
+            status=SpecificationDefinitionStatus.OBSOLETE)
+        self.factory.makeSpecification(
+            distribution=distribution, name="spec4")
+        distro_on_webservice = self.getPillarOnWebservice(distribution)
+        distroseries_on_webservice = distro_on_webservice.getSeries(
+            name_or_version="maudlin")
+        self.assertNamesOfSpecificationsAre(
+            ["spec1", "spec2"],
+            distroseries_on_webservice.valid_specifications)
+
+    def test_productseries_all_specifications(self):
+        product = self.makeProduct()
+        productseries = self.factory.makeProductSeries(
+            product=product, name="fooix-dev")
+        self.factory.makeSpecification(
+            product=product, name="spec1", goal=productseries)
+        self.factory.makeSpecification(
+            product=product, name="spec2", goal=productseries)
+        self.factory.makeSpecification(product=product, name="spec3")
+        product_on_webservice = self.getPillarOnWebservice(product)
+        series_on_webservice = product_on_webservice.getSeries(
+            name="fooix-dev")
+        self.assertNamesOfSpecificationsAre(
+            ["spec1", "spec2"], series_on_webservice.all_specifications)
+
+    def test_productseries_valid_specifications(self):
+        product = self.makeProduct()
+        productseries = self.factory.makeProductSeries(
+            product=product, name="fooix-dev")
+        self.factory.makeSpecification(
+            product=product, name="spec1", goal=productseries)
+        self.factory.makeSpecification(
+            product=product, name="spec2", goal=productseries)
+        self.factory.makeSpecification(
+            product=product, name="spec3", goal=productseries,
+            status=SpecificationDefinitionStatus.OBSOLETE)
+        self.factory.makeSpecification(product=product, name="spec4")
+        product_on_webservice = self.getPillarOnWebservice(product)
+        series_on_webservice = product_on_webservice.getSeries(
+            name="fooix-dev")
+        # Should this be different to the results for distroseries?
+        self.assertNamesOfSpecificationsAre(
+            ["spec1", "spec2"],
+            series_on_webservice.valid_specifications)
+
+    def test_projectgroup_all_specifications(self):
+        projectgroup = self.factory.makeProject()
+        other_projectgroup = self.factory.makeProject()
+        product1 = self.factory.makeProduct(project=projectgroup)
+        product2 = self.factory.makeProduct(project=projectgroup)
+        product3 = self.factory.makeProduct(project=other_projectgroup)
+        self.factory.makeSpecification(
+            product=product1, name="spec1")
+        self.factory.makeSpecification(
+            product=product2, name="spec2",
+            status=SpecificationDefinitionStatus.OBSOLETE)
+        self.factory.makeSpecification(
+            product=product3, name="spec3")
+        projectgroup_on_webservice = self.getPillarOnWebservice(projectgroup)
+        # Should this be different to the results for distroseries?
+        self.assertNamesOfSpecificationsAre(
+            ["spec1", "spec2"],
+            projectgroup_on_webservice.all_specifications)
+
+    def test_projectgroup_valid_specifications(self):
+        projectgroup = self.factory.makeProject()
+        other_projectgroup = self.factory.makeProject()
+        product1 = self.factory.makeProduct(project=projectgroup)
+        product2 = self.factory.makeProduct(project=projectgroup)
+        product3 = self.factory.makeProduct(project=other_projectgroup)
+        self.factory.makeSpecification(
+            product=product1, name="spec1")
+        self.factory.makeSpecification(
+            product=product2, name="spec2",
+            status=SpecificationDefinitionStatus.OBSOLETE)
+        self.factory.makeSpecification(
+            product=product3, name="spec3")
+        projectgroup_on_webservice = self.getPillarOnWebservice(projectgroup)
+        # Should this be different to the results for distroseries?
+        self.assertNamesOfSpecificationsAre(
+            ["spec1", "spec2"],
+            projectgroup_on_webservice.valid_specifications)
+
+    def test_person_all_specifications(self):
+        person = self.factory.makePerson(name="james-w")
+        product = self.factory.makeProduct()
+        self.factory.makeSpecification(
+            product=product, name="spec1", drafter=person)
+        self.factory.makeSpecification(
+            product=product, name="spec2", approver=person,
+            status=SpecificationDefinitionStatus.OBSOLETE)
+        self.factory.makeSpecification(
+            product=product, name="spec3")
+        launchpadlib = self.getLaunchpadlib()
+        person_on_webservice = launchpadlib.load(
+            str(launchpadlib._root_uri) + '/~james-w')
+        self.assertNamesOfSpecificationsAre(
+            ["spec1", "spec2"], person_on_webservice.all_specifications)
+
+    def test_person_valid_specifications(self):
+        person = self.factory.makePerson(name="james-w")
+        product = self.factory.makeProduct()
+        self.factory.makeSpecification(
+            product=product, name="spec1", drafter=person)
+        self.factory.makeSpecification(
+            product=product, name="spec2", approver=person,
+            status=SpecificationDefinitionStatus.OBSOLETE)
+        self.factory.makeSpecification(
+            product=product, name="spec3")
+        launchpadlib = self.getLaunchpadlib()
+        person_on_webservice = launchpadlib.load(
+            str(launchpadlib._root_uri) + '/~james-w')
+        self.assertNamesOfSpecificationsAre(
+            ["spec1"], person_on_webservice.valid_specifications)

=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py	2010-11-18 12:05:34 +0000
+++ lib/lp/testing/factory.py	2010-11-25 19:32:28 +0000
@@ -102,7 +102,11 @@
 from lp.app.enums import ServiceUsage
 from lp.archiveuploader.dscfile import DSCFile
 from lp.archiveuploader.uploadpolicy import BuildDaemonUploadPolicy
-from lp.blueprints.enums import SpecificationDefinitionStatus
+from lp.blueprints.enums import (
+    SpecificationDefinitionStatus,
+    SpecificationGoalStatus,
+    SpecificationPriority,
+    )
 from lp.blueprints.interfaces.specification import ISpecificationSet
 from lp.blueprints.interfaces.sprint import ISprintSet
 from lp.bugs.interfaces.bug import (
@@ -1671,7 +1675,9 @@
     def makeSpecification(self, product=None, title=None, distribution=None,
                           name=None, summary=None, owner=None,
                           status=SpecificationDefinitionStatus.NEW,
-                          implementation_status=None):
+                          implementation_status=None, goal=None, specurl=None,
+                          assignee=None, drafter=None, approver=None,
+                          priority=None, whiteboard=None, milestone=None):
         """Create and return a new, arbitrary Blueprint.
 
         :param product: The product to make the blueprint on.  If one is
@@ -1687,17 +1693,32 @@
             title = self.getUniqueString('title')
         if owner is None:
             owner = self.makePerson()
+        if priority is None:
+            priority = SpecificationPriority.UNDEFINED
         spec = getUtility(ISpecificationSet).new(
             name=name,
             title=title,
             specurl=None,
             summary=summary,
             definition_status=status,
+            whiteboard=whiteboard,
             owner=owner,
+            assignee=assignee,
+            drafter=drafter,
+            approver=approver,
             product=product,
-            distribution=distribution)
+            distribution=distribution,
+            priority=priority)
+        naked_spec = removeSecurityProxy(spec)
+        if status == SpecificationDefinitionStatus.OBSOLETE:
+            # This is to satisfy a DB constraint of obsolete specs.
+            naked_spec.completer = owner
+            naked_spec.date_completed = datetime.now(pytz.UTC)
+        naked_spec.specurl = specurl
+        naked_spec.milestone = milestone
+        if goal is not None:
+            naked_spec.proposeGoal(goal, spec.target.owner)
         if implementation_status is not None:
-            naked_spec = removeSecurityProxy(spec)
             naked_spec.implementation_status = implementation_status
             naked_spec.updateLifecycleStatus(owner)
         return spec


Follow ups