← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~james-w/launchpad/expose-blueprints into lp:launchpad/devel

 

James Westby has proposed merging lp:~james-w/launchpad/expose-blueprints into lp:launchpad/devel.

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


Hi,

Summary

Expose some blueprint attributes and a couple of methods over the API.

Proposed fix

Add exported() in a bunch of places.

Pre-implementation notes

None.

Implementation details

It exports the basics of ISpecification, plus 3 methods that may be useful
to people for getting blueprints.

I added two new methods to IHasSpecifications, as if we exposed the existing
attributes we would serve the collections when just getting the specification.

I'm not entirely sure what getValidSpecifications means, given its apparently
inconsistent implementations, so perhaps it is not worth exposing it.

A getSpecifications() similar to searchTasks() would be great, but the API isn't
there for that.

Also, I guess there may be opposition to exposing the whiteboard, but I think
if it is there then it should be exposed, and as it will be one of the most
used attributes not having it would make the API close to useless, at least for
removing the screen scraping in launchpad-work-items-tracker.

Tests

./bin/test -s lp.blueprints.tests -m test_webservice

Demo and Q/A

I'll test it with launchpadlib once it is on edge.

lint

./bin/lint.sh: line 161: pocketlint: command not found

and I don't know where to get it.
-- 
https://code.launchpad.net/~james-w/launchpad/expose-blueprints/+merge/30026
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~james-w/launchpad/expose-blueprints into lp:launchpad/devel.
=== modified file 'lib/canonical/launchpad/doc/tales.txt'
--- lib/canonical/launchpad/doc/tales.txt	2010-05-21 14:58:50 +0000
+++ lib/canonical/launchpad/doc/tales.txt	2010-07-15 15:52:48 +0000
@@ -669,8 +669,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>'
 
@@ -678,7 +681,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/canonical/launchpad/interfaces/_schema_circular_imports.py'
--- lib/canonical/launchpad/interfaces/_schema_circular_imports.py	2010-07-13 15:29:08 +0000
+++ lib/canonical/launchpad/interfaces/_schema_circular_imports.py	2010-07-15 15:52:48 +0000
@@ -43,6 +43,8 @@
 from lp.blueprints.interfaces.specification import ISpecification
 from lp.blueprints.interfaces.specificationbranch import (
     ISpecificationBranch)
+from lp.blueprints.interfaces.specificationtarget import (
+    IHasSpecifications, ISpecificationTarget)
 from lp.code.interfaces.branch import IBranch
 from lp.code.interfaces.branchmergeproposal import IBranchMergeProposal
 from lp.code.interfaces.branchsubscription import IBranchSubscription
@@ -433,3 +435,13 @@
 
 # IProductSeries
 patch_reference_property(IProductSeries, 'product', IProduct)
+
+# IHasSpecifications
+patch_collection_return_type(
+        IHasSpecifications, 'getAllSpecifications', ISpecification)
+patch_collection_return_type(
+        IHasSpecifications, 'getValidSpecifications', ISpecification)
+
+# ISpecificationTarget
+patch_entry_return_type(
+    ISpecificationTarget, 'getSpecification', ISpecification)

=== modified file 'lib/lp/blueprints/interfaces/specification.py'
--- lib/lp/blueprints/interfaces/specification.py	2010-02-19 12:05:10 +0000
+++ lib/lp/blueprints/interfaces/specification.py	2010-07-15 15:52:48 +0000
@@ -27,27 +27,31 @@
 
 
 from lazr.restful.declarations import (
-    REQUEST_USER, call_with, export_as_webservice_entry,
-    export_write_operation, operation_parameters, operation_returns_entry)
-from lazr.restful.fields import Reference
+    exported, export_as_webservice_entry)
+from lazr.restful.fields import ReferenceChoice
 from zope.interface import Interface, Attribute
 from zope.component import getUtility
 
 from zope.schema import Datetime, Int, Choice, Text, TextLine, Bool
 
 from canonical.launchpad import _
+from canonical.launchpad.interfaces.validation import valid_webref
 from canonical.launchpad.fields import (
     ContentNameField, PublicPersonChoice, Summary, Title)
 from canonical.launchpad.validators import LaunchpadValidationError
-from lp.registry.interfaces.role import IHasOwner
-from lp.code.interfaces.branch import IBranch
+from lp.blueprints.interfaces.sprint import ISprint
+from lp.blueprints.interfaces.specificationtarget import (
+    IHasSpecifications)
 from lp.code.interfaces.branchlink import IHasLinkedBranches
+from lp.registry.interfaces.distribution import IDistribution
+from lp.registry.interfaces.distroseries import IDistroSeries
 from lp.registry.interfaces.mentoringoffer import ICanBeMentored
-from canonical.launchpad.interfaces.validation import valid_webref
+from lp.registry.interfaces.milestone import IMilestone
 from lp.registry.interfaces.projectgroup import IProjectGroup
-from lp.blueprints.interfaces.sprint import ISprint
-from lp.blueprints.interfaces.specificationtarget import (
-    IHasSpecifications)
+from lp.registry.interfaces.product import IProduct
+from lp.registry.interfaces.productseries import IProductSeries
+from lp.registry.interfaces.role import IHasOwner
+
 
 from lazr.enum import (
     DBEnumeratedType, DBItem, EnumeratedType, Item)
@@ -554,49 +558,58 @@
 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.")))
+    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.")))
+    specurl = exported(
+        SpecURLField(
+            title=_('Specification URL'), required=False,
+            description=_(
+                "The URL of the specification. This is usually a wiki page."),
+            constraint=valid_webref),
+        exported_as="specification_url")
+    summary = exported(
+        Summary(
+            title=_('Summary'), required=True, description=_(
+                "A single-paragraph description of the feature. "
+                "This will also be displayed in most feature listings.")))
+    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.")))
+    assignee = exported(
+            PublicPersonChoice(
+            title=_('Assignee'), required=False,
+            description=_(
+                "The person responsible for implementing the feature."),
+            vocabulary='ValidPersonOrTeam'))
+    drafter = exported(
+            PublicPersonChoice(
+            title=_('Drafter'), required=False,
+            description=_(
+                    "The person responsible for drafting the specification."),
+                vocabulary='ValidPersonOrTeam'))
+    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'))
 
 
 class INewSpecificationProjectTarget(Interface):
@@ -645,7 +658,10 @@
 
 class ISpecification(INewSpecification, INewSpecificationTarget, IHasOwner,
     ICanBeMentored, IHasLinkedBranches):
-    """A Specification."""
+    """A Specification.
+
+    Also known as a blueprint.
+    """
 
     export_as_webservice_entry()
 
@@ -655,46 +671,62 @@
     #      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')
+    priority = exported(
+        Choice(
+            title=_('Priority'), vocabulary=SpecificationPriority,
+            default=SpecificationPriority.UNDEFINED, required=True))
+    datecreated = exported(
+        Datetime(
+            title=_('Date Created'), required=True, readonly=True),
+        exported_as='date_created')
+    owner = exported(
+        PublicPersonChoice(
+            title=_('Owner'), required=True, readonly=True,
+            vocabulary='ValidPersonOrTeam'))
     # target
-    product = Choice(title=_('Project'), required=False,
-        vocabulary='Product')
-    distribution = Choice(title=_('Distribution'), required=False,
-        vocabulary='Distribution')
+    product = exported(
+        ReferenceChoice(title=_('Project'), required=False,
+               vocabulary='Product', schema=IProduct),
+        exported_as='project')
+    distribution = exported(
+        ReferenceChoice(title=_('Distribution'), required=False,
+               vocabulary='Distribution', schema=IDistribution))
 
     # series
-    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,
-        vocabulary='FilteredDistroSeries',
-        description=_(
-            "Choose a series in which you would like to deliver "
-            "this feature. Selecting '(no value)' will clear the goal."))
+    productseries = exported(
+        ReferenceChoice(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."),
+               schema=IProductSeries),
+        exported_as='project_series')
+    distroseries = exported(
+        ReferenceChoice(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."),
+               schema=IDistroSeries))
 
     # 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))
 
     # nomination to a series for release management
     goal = Attribute("The series for which this feature is a goal.")
-    goalstatus = Choice(
-        title=_('Goal Acceptance'), vocabulary=SpecificationGoalStatus,
-        default=SpecificationGoalStatus.PROPOSED, description=_(
-            "Whether or not the drivers have accepted this feature as "
-            "a goal for the targeted series."))
+    goalstatus = exported(
+        Choice(
+            title=_('Goal Acceptance'), vocabulary=SpecificationGoalStatus,
+            default=SpecificationGoalStatus.PROPOSED, description=_(
+                "Whether or not the drivers have accepted this feature as "
+                "a goal for the targeted series.")),
+        exported_as='goal_status')
     goal_proposer = Attribute("The person who nominated the spec for "
         "this series.")
     date_goal_proposed = Attribute("The date of the nomination.")
@@ -703,10 +735,11 @@
     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.")))
     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	2009-08-20 14:00:52 +0000
+++ lib/lp/blueprints/interfaces/specificationtarget.py	2010-07-15 15:52:48 +0000
@@ -14,6 +14,12 @@
     ]
 
 from zope.interface import Interface, Attribute
+from zope.schema import TextLine
+
+from canonical.launchpad import _
+from lazr.restful.declarations import (
+    export_read_operation, operation_parameters,
+    operation_returns_collection_of, operation_returns_entry)
 
 
 class IHasSpecifications(Interface):
@@ -59,6 +65,15 @@
         situations in which these are not rendered.
         """
 
+    @operation_returns_collection_of(Interface) # really ISpecification
+    @export_read_operation()
+    def getAllSpecifications():
+        """Return all the specifications associated with this object."""
+
+    @operation_returns_collection_of(Interface) # really ISpecification
+    @export_read_operation()
+    def getValidSpecifications():
+        """Return all the non-obsolete specifications for this object."""
 
 
 class ISpecificationTarget(IHasSpecifications):
@@ -66,6 +81,11 @@
     specifications directly attached to them.
     """
 
+    @operation_parameters(
+        name=TextLine(title=_('The name of the specification'))
+    )
+    @operation_returns_entry(Interface) # really ISpecification
+    @export_read_operation()
     def getSpecification(name):
         """Returns the specification with the given name, for this target,
         or None.

=== modified file 'lib/lp/blueprints/model/specification.py'
--- lib/lp/blueprints/model/specification.py	2009-09-21 14:56:07 +0000
+++ lib/lp/blueprints/model/specification.py	2010-07-15 15:52:48 +0000
@@ -685,6 +685,14 @@
         """See IHasSpecifications."""
         return self.specifications(filter=[SpecificationFilter.ALL]).count()
 
+    def getAllSpecifications(self):
+        """See IHasSpecifications."""
+        return self.all_specifications
+
+    def getValidSpecifications(self):
+        """See IHasSpecifications."""
+        return self.valid_specifications
+
 
 class SpecificationSet(HasSpecificationsMixin):
     """The set of feature specifications."""
@@ -838,13 +846,23 @@
     def new(self, name, title, specurl, summary, definition_status,
         owner, approver=None, product=None, distribution=None, assignee=None,
         drafter=None, whiteboard=None,
-        priority=SpecificationPriority.UNDEFINED):
+        priority=SpecificationPriority.UNDEFINED,
+        goalstatus=SpecificationGoalStatus.PROPOSED,
+        productseries=None, distroseries=None,
+        goal_proposer=None, date_goal_proposed=None, milestone=None,
+        date_completed=None, completer=None, goal_decider=None,
+        date_goal_decided=None):
         """See ISpecificationSet."""
         return Specification(name=name, title=title, specurl=specurl,
             summary=summary, priority=priority,
             definition_status=definition_status, owner=owner,
             approver=approver, product=product, distribution=distribution,
-            assignee=assignee, drafter=drafter, whiteboard=whiteboard)
+            assignee=assignee, drafter=drafter, whiteboard=whiteboard,
+            goalstatus=goalstatus, productseries=productseries,
+            distroseries=distroseries, goal_proposer=goal_proposer,
+            date_goal_proposed=date_goal_proposed, milestone=milestone,
+            date_completed=date_completed, completer=completer,
+            goal_decider=goal_decider, date_goal_decided=date_goal_decided)
 
     def getDependencyDict(self, specifications):
         """See `ISpecificationSet`."""

=== 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-07-15 15:52:48 +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-07-15 15:52:48 +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-07-15 15:52:48 +0000
@@ -0,0 +1,550 @@
+# 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, SpecificationGoalStatus,
+    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)
+
+    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."
+        definition_status = SpecificationDefinitionStatus.PENDINGAPPROVAL
+        self.definition_status = definition_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
+        goal_status = SpecificationGoalStatus.PROPOSED
+        self.goal_status = goal_status.title
+        self.whiteboard = "Some whiteboard"
+        product = self.factory.makeProduct()
+        return self.factory.makeSpecification(
+            product=product, name=self.name,
+            title=self.title, specurl=self.url,
+            summary=self.summary,
+            definition_status=definition_status,
+            assignee=assignee, drafter=drafter, approver=approver,
+            priority=priority,
+            owner=owner, whiteboard=self.whiteboard, goalstatus=goal_status)
+
+    def getSimpleSpecificationResponse(self):
+        self.spec_object = self.makeSimpleSpecification()
+        return self.getSpecOnWebservice(self.spec_object)
+
+    def test_can_retrieve_representation(self):
+        spec = self.makeSimpleSpecification()
+        user = self.factory.makePerson()
+        webservice = webservice_for_person(user)
+        response = webservice.get(
+            '/%s/+spec/%s' % (spec.product.name, spec.name))
+        self.assertEqual(response.status, 200)
+
+    def test_representation_contains_name(self):
+        spec = self.getSimpleSpecificationResponse()
+        self.assertEqual(self.name, spec.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_goal_status(self):
+        spec = self.getSimpleSpecificationResponse()
+        self.assertEqual(self.goal_status, spec.goal_status)
+
+    def test_representation_contains_whiteboard(self):
+        spec = self.getSimpleSpecificationResponse()
+        self.assertEqual(self.whiteboard, spec.whiteboard)
+
+    def test_representation_with_no_whiteboard(self):
+        product = self.makeProduct()
+        name = "some-spec"
+        spec_object = self.factory.makeSpecification(
+            product=product, name=name, whiteboard=None)
+        # Check that the factory didn't add a whiteboard
+        self.assertEqual(None, spec_object.whiteboard)
+        spec = self.getSpecOnWebservice(spec_object)
+        # Check that it is None on the webservice too
+        self.assertEqual(None, spec.whiteboard)
+
+    def test_representation_with_no_approver(self):
+        product = self.makeProduct()
+        name = "some-spec"
+        spec_object = self.factory.makeSpecification(
+            product=product, name=name, approver=None)
+        # Check that the factory didn't add an approver
+        self.assertEqual(None, spec_object.approver)
+        spec = self.getSpecOnWebservice(spec_object)
+        # Check that it is None on the webservice too
+        self.assertEqual(None, spec.approver)
+
+    def test_representation_with_no_drafter(self):
+        product = self.makeProduct()
+        name = "some-spec"
+        spec_object = self.factory.makeSpecification(
+            product=product, name=name, drafter=None)
+        # Check that the factory didn't add an drafter
+        self.assertEqual(None, spec_object.drafter)
+        spec = self.getSpecOnWebservice(spec_object)
+        # Check that it is None on the webservice too
+        self.assertEqual(None, spec.drafter)
+
+    def test_representation_with_no_assignee(self):
+        product = self.makeProduct()
+        name = "some-spec"
+        spec_object = self.factory.makeSpecification(
+            product=product, name=name, assignee=None)
+        # Check that the factory didn't add an assignee
+        self.assertEqual(None, spec_object.assignee)
+        spec = self.getSpecOnWebservice(spec_object)
+        # Check that it is None on the webservice too
+        self.assertEqual(None, spec.assignee)
+
+    def test_representation_with_no_specification_url(self):
+        product = self.makeProduct()
+        name = "some-spec"
+        spec_object = self.factory.makeSpecification(
+            product=product, name=name, specurl=None)
+        # Check that the factory didn't add an specurl
+        self.assertEqual(None, spec_object.specurl)
+        spec = self.getSpecOnWebservice(spec_object)
+        # Check that it is None on the webservice too
+        self.assertEqual(None, spec.specification_url)
+
+    def test_representation_has_project_link(self):
+        product = self.makeProduct()
+        name = "some-spec"
+        spec_object = self.factory.makeSpecification(
+            product=product, name=name)
+        spec = self.getSpecOnWebservice(spec_object)
+        self.assertEqual('fooix', spec.project.name)
+
+    def test_representation_has_project_series_link(self):
+        product = self.makeProduct()
+        productseries = self.factory.makeProductSeries(
+            name='fooix-dev', product=product)
+        name = "some-spec"
+        spec_object = self.factory.makeSpecification(
+            product=product, name=name, productseries=productseries)
+        spec = self.getSpecOnWebservice(spec_object)
+        self.assertEqual('fooix-dev', spec.project_series.name)
+
+    def test_representation_has_distribution_link(self):
+        distribution = self.makeDistribution()
+        name = "some-spec"
+        spec_object = self.factory.makeSpecification(
+            distribution=distribution, name=name)
+        spec = self.getSpecOnWebservice(spec_object)
+        self.assertEqual('foobuntu', spec.distribution.name)
+
+    def test_representation_has_distroseries_link(self):
+        distribution = self.makeDistribution()
+        distroseries = self.factory.makeDistroSeries(
+            name='maudlin', distribution=distribution)
+        name = "some-spec"
+        spec_object = self.factory.makeSpecification(
+            distribution=distribution, name=name, distroseries=distroseries)
+        spec = self.getSpecOnWebservice(spec_object)
+        self.assertEqual('maudlin', spec.distroseries.name)
+
+    def test_representation_empty_distribution(self):
+        product = self.makeProduct()
+        name = "some-spec"
+        spec_object = self.factory.makeSpecification(
+            product=product, name=name)
+        # Check that we didn't pick one up in the factory
+        self.assertEqual(None, spec_object.distribution)
+        spec = self.getSpecOnWebservice(spec_object)
+        self.assertEqual(None, spec.distribution)
+
+    def test_representation_empty_project_series(self):
+        product = self.makeProduct()
+        name = "some-spec"
+        spec_object = self.factory.makeSpecification(
+            product=product, name=name)
+        # Check that we didn't pick one up in the factory
+        self.assertEqual(None, spec_object.productseries)
+        spec = self.getSpecOnWebservice(spec_object)
+        self.assertEqual(None, spec.project_series)
+
+    def test_representation_empty_project(self):
+        distribution = self.makeDistribution()
+        name = "some-spec"
+        spec_object = self.factory.makeSpecification(
+            distribution=distribution, name=name)
+        # Check that we didn't pick one up in the factory
+        self.assertEqual(None, spec_object.product)
+        spec = self.getSpecOnWebservice(spec_object)
+        self.assertEqual(None, spec.project)
+
+    def test_representation_empty_distroseries(self):
+        distribution = self.makeDistribution()
+        name = "some-spec"
+        spec_object = self.factory.makeSpecification(
+            distribution=distribution, name=name)
+        # Check that we didn't pick one up in the factory
+        self.assertEqual(None, spec_object.distroseries)
+        spec = self.getSpecOnWebservice(spec_object)
+        self.assertEqual(None, spec.distroseries)
+
+    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, productseries=productseries, milestone=milestone)
+        spec = self.getSpecOnWebservice(spec_object)
+        self.assertEqual("1.0", spec.milestone.name)
+
+    def test_representation_empty_milestone(self):
+        product = self.makeProduct()
+        spec_object = self.factory.makeSpecification(
+            product=product, milestone=None)
+        # Check that the factory didn't add a milestone
+        self.assertEqual(None, spec_object.milestone)
+        spec = self.getSpecOnWebservice(spec_object)
+        self.assertEqual(None, spec.milestone)
+
+
+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.project.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)
+
+    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.distribution.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", productseries=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.project.name)
+        self.assertEqual("fooix-dev", spec.project_series.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",
+            distroseries=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.distribution.name)
+        self.assertEqual("maudlin", spec.distroseries.name)
+
+
+class IHasSpecificationsTests(SpecificationWebserviceTestCase):
+    """Tests for accessing IHasSpecifications methods over the webservice."""
+    layer = DatabaseFunctionalLayer
+
+    def assertNamesOfSpecificationsAre(self, names, specifications):
+        self.assertEqual(names, [s.name for s in specifications])
+
+    def test_product_getAllSpecifications(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.getAllSpecifications())
+
+    def test_product_getValidSpecifications(self):
+        product = self.makeProduct()
+        self.factory.makeSpecification(product=product, name="spec1")
+        self.factory.makeSpecification(
+            product=product, name="spec2",
+            definition_status=SpecificationDefinitionStatus.OBSOLETE)
+        product_on_webservice = self.getPillarOnWebservice(product)
+        self.assertNamesOfSpecificationsAre(
+            ["spec1"], product_on_webservice.getValidSpecifications())
+
+    def test_distribution_getAllSpecifications(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.getAllSpecifications())
+
+    def test_distribution_getValidSpecifications(self):
+        distribution = self.makeDistribution()
+        self.factory.makeSpecification(
+            distribution=distribution, name="spec1")
+        self.factory.makeSpecification(
+            distribution=distribution, name="spec2",
+            definition_status=SpecificationDefinitionStatus.OBSOLETE)
+        distro_on_webservice = self.getPillarOnWebservice(distribution)
+        self.assertNamesOfSpecificationsAre(
+            ["spec1"], distro_on_webservice.getValidSpecifications())
+
+    def test_distroseries_getAllSpecifications(self):
+        distribution = self.makeDistribution()
+        distroseries = self.factory.makeDistroSeries(
+            name='maudlin', distribution=distribution)
+        self.factory.makeSpecification(
+            distribution=distribution, name="spec1",
+            distroseries=distroseries)
+        self.factory.makeSpecification(
+            distribution=distribution, name="spec2",
+            distroseries=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.getAllSpecifications())
+
+    def test_distroseries_getValidSpecifications(self):
+        distribution = self.makeDistribution()
+        distroseries = self.factory.makeDistroSeries(
+            name='maudlin', distribution=distribution)
+        self.factory.makeSpecification(
+            distribution=distribution, name="spec1",
+            distroseries=distroseries,
+            goalstatus=SpecificationGoalStatus.ACCEPTED)
+        self.factory.makeSpecification(
+            distribution=distribution, name="spec2",
+            goalstatus=SpecificationGoalStatus.DECLINED,
+            distroseries=distroseries)
+        self.factory.makeSpecification(
+            distribution=distribution, name="spec3",
+            distroseries=distroseries,
+            goalstatus=SpecificationGoalStatus.ACCEPTED,
+            definition_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", "spec3"],
+            distroseries_on_webservice.getValidSpecifications())
+
+    def test_productseries_getAllSpecifications(self):
+        product = self.makeProduct()
+        productseries = self.factory.makeProductSeries(
+            product=product, name="fooix-dev")
+        self.factory.makeSpecification(
+            product=product, name="spec1", productseries=productseries)
+        self.factory.makeSpecification(
+            product=product, name="spec2", productseries=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.getAllSpecifications())
+
+    def test_productseries_getValidSpecifications(self):
+        product = self.makeProduct()
+        productseries = self.factory.makeProductSeries(
+            product=product, name="fooix-dev")
+        self.factory.makeSpecification(
+            product=product, name="spec1", productseries=productseries,
+            goalstatus=SpecificationGoalStatus.ACCEPTED)
+        self.factory.makeSpecification(
+            goalstatus=SpecificationGoalStatus.DECLINED,
+            product=product, name="spec2", productseries=productseries)
+        self.factory.makeSpecification(
+            product=product, name="spec3", productseries=productseries,
+            goalstatus=SpecificationGoalStatus.ACCEPTED,
+            definition_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", "spec3"],
+            series_on_webservice.getAllSpecifications())
+
+    def test_projectgroup_getAllSpecifications(self):
+        productgroup = self.factory.makeProject()
+        other_productgroup = self.factory.makeProject()
+        product1 = self.factory.makeProduct(project=productgroup)
+        product2 = self.factory.makeProduct(project=productgroup)
+        product3 = self.factory.makeProduct(project=other_productgroup)
+        self.factory.makeSpecification(
+            product=product1, name="spec1")
+        self.factory.makeSpecification(
+            product=product2, name="spec2",
+            definition_status=SpecificationDefinitionStatus.OBSOLETE)
+        self.factory.makeSpecification(
+            product=product3, name="spec3")
+        product_on_webservice = self.getPillarOnWebservice(productgroup)
+        # Should this be different to the results for distroseries?
+        self.assertNamesOfSpecificationsAre(
+            ["spec1", "spec2"],
+            product_on_webservice.getAllSpecifications())
+
+    def test_projectgroup_getValidSpecifications(self):
+        productgroup = self.factory.makeProject()
+        other_productgroup = self.factory.makeProject()
+        product1 = self.factory.makeProduct(project=productgroup)
+        product2 = self.factory.makeProduct(project=productgroup)
+        product3 = self.factory.makeProduct(project=other_productgroup)
+        self.factory.makeSpecification(
+            product=product1, name="spec1")
+        self.factory.makeSpecification(
+            product=product2, name="spec2",
+            definition_status=SpecificationDefinitionStatus.OBSOLETE)
+        self.factory.makeSpecification(
+            product=product3, name="spec3")
+        product_on_webservice = self.getPillarOnWebservice(productgroup)
+        # Should this be different to the results for distroseries?
+        self.assertNamesOfSpecificationsAre(
+            ["spec1", "spec2"],
+            product_on_webservice.getValidSpecifications())
+
+    def test_person_getAllSpecifications(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,
+            definition_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.getAllSpecifications())
+
+    def test_person_getValidSpecifications(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,
+            definition_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.getAllSpecifications())

=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py	2010-07-14 19:33:16 +0000
+++ lib/lp/testing/factory.py	2010-07-15 15:52:48 +0000
@@ -67,7 +67,8 @@
 from lp.archiveuploader.dscfile import DSCFile
 from lp.archiveuploader.uploadpolicy import BuildDaemonUploadPolicy
 from lp.blueprints.interfaces.specification import (
-    ISpecificationSet, SpecificationDefinitionStatus)
+    ISpecificationSet, SpecificationDefinitionStatus, SpecificationGoalStatus,
+    SpecificationPriority)
 from lp.blueprints.interfaces.sprint import ISprintSet
 
 from lp.bugs.interfaces.bug import CreateBugParams, IBugSet
@@ -1369,7 +1370,11 @@
         mail.parsed_string = mail.as_string()
         return mail
 
-    def makeSpecification(self, product=None, title=None, distribution=None):
+    def makeSpecification(self, product=None, title=None, distribution=None,
+            name=None, specurl=None, summary=None, definition_status=None,
+            assignee=None, drafter=None, approver=None, priority=None,
+            owner=None, goalstatus=None, whiteboard=None, productseries=None,
+            distroseries=None, milestone=None):
         """Create and return a new, arbitrary Blueprint.
 
         :param product: The product to make the blueprint on.  If one is
@@ -1379,15 +1384,58 @@
             product = self.makeProduct()
         if title is None:
             title = self.getUniqueString('title')
+        if name is None:
+            name = self.getUniqueString('name')
+        if summary is None:
+            summary = self.getUniqueString('summary')
+        if definition_status is None:
+            definition_status = SpecificationDefinitionStatus.NEW
+        if priority is None:
+            priority = SpecificationPriority.LOW
+        if goalstatus is None:
+            goalstatus = SpecificationGoalStatus.PROPOSED
+        if owner is None:
+            owner = self.makePerson()
+        goal_proposer = None
+        date_goal_proposed = None
+        if distroseries is not None or productseries is not None:
+            goal_proposer = self.makePerson()
+            date_goal_proposed = datetime.now(pytz.UTC)
+        goal_decider = None
+        date_goal_decided = None
+        if goalstatus != SpecificationGoalStatus.PROPOSED:
+            goal_decider = self.makePerson()
+            date_goal_decided = datetime.now(pytz.UTC)
+        completer = None
+        date_completed = None
+        if definition_status == SpecificationDefinitionStatus.OBSOLETE:
+            completer = self.makePerson()
+            date_completed = datetime.now(pytz.UTC)
         return getUtility(ISpecificationSet).new(
-            name=self.getUniqueString('name'),
+            name=name,
             title=title,
-            specurl=None,
-            summary=self.getUniqueString('summary'),
-            definition_status=SpecificationDefinitionStatus.NEW,
-            owner=self.makePerson(),
+            specurl=specurl,
+            summary=summary,
+            definition_status=definition_status,
+            owner=owner,
             product=product,
-            distribution=distribution)
+            productseries=productseries,
+            distribution=distribution,
+            distroseries=distroseries,
+            assignee=assignee,
+            drafter=drafter,
+            approver=approver,
+            priority=priority,
+            goalstatus=goalstatus,
+            whiteboard=whiteboard,
+            goal_proposer=goal_proposer,
+            date_goal_proposed=date_goal_proposed,
+            milestone=milestone,
+            date_completed=date_completed,
+            completer=completer,
+            goal_decider=goal_decider,
+            date_goal_decided=date_goal_decided,
+            )
 
     def makeQuestion(self, target=None, title=None):
         """Create and return a new, arbitrary Question.


Follow ups