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

Proposed by Guilherme Salgado
Status: Merged
Approved by: Guilherme Salgado
Approved revision: no longer in the source branch.
Merged at revision: 11997
Proposed branch: lp:~salgado/launchpad/expose-blueprints
Merge into: lp:launchpad
Diff against target: 1054 lines (+690/-99)
11 files modified
lib/canonical/launchpad/interfaces/_schema_circular_imports.py (+14/-0)
lib/lp/app/doc/tales.txt (+6/-2)
lib/lp/blueprints/interfaces/specification.py (+133/-78)
lib/lp/blueprints/interfaces/specificationtarget.py (+42/-6)
lib/lp/blueprints/interfaces/webservice.py (+1/-0)
lib/lp/blueprints/model/specification.py (+5/-2)
lib/lp/blueprints/model/sprint.py (+4/-3)
lib/lp/blueprints/stories/standalone/sprint-links.txt (+0/-4)
lib/lp/blueprints/tests/test_implements.py (+65/-0)
lib/lp/blueprints/tests/test_webservice.py (+395/-0)
lib/lp/testing/factory.py (+25/-4)
To merge this branch: bzr merge lp:~salgado/launchpad/expose-blueprints
Reviewer Review Type Date Requested Status
Michael Hudson-Doyle Approve
Review via email: mp+41898@code.launchpad.net

Commit message

[r=mwhudson][ui=none][bug=146389] Start exposing ISpecification and IHasSpecifications attributes on the webservice API.

Description of the change

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.

To post a comment you must log in.
Revision history for this message
Guilherme Salgado (salgado) wrote :

My last commit removes another controversial bit by exposing ISpecification.definition_status as read only. That attribute will probably need a mutator (which in turn requires some refactoring) before it can be exported.

Revision history for this message
Michael Hudson-Doyle (mwhudson) wrote :
Download full text (33.7 KiB)

Hi Salgado,

Yay for progress on this issue. I entirely support the postponing of
all the difficult stuff :-)

I have some quibbles about the tests, but nothing serious I hope.

> === 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 22:50:47 +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)

It's a shame that we don't have the app specific circular import files
yet... oh well.

> === 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 22:50:47 +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,69 @@
> 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 "
> - ...

review: Approve
Revision history for this message
Guilherme Salgado (salgado) wrote :
Download full text (21.8 KiB)

Hi Michael,

Thanks for the review!

On Fri, 2010-11-26 at 01:12 +0000, Michael Hudson-Doyle wrote:
> Review: Approve
> Hi Salgado,
>
> Yay for progress on this issue. I entirely support the postponing of
> all the difficult stuff :-)
>
> I have some quibbles about the tests, but nothing serious I hope.

I like all your suggestions and since it's Friday and I'm all excited
with the weekend approaching, I'm proposing even more changes. :)

>
> > === 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 22:50:47 +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)
>
> It's a shame that we don't have the app specific circular import files
> yet... oh well.

That'd be a slight improvement, but it's still a PITA to maintain these
"patches".

> > === 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 22:50:47 +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")
>
> I don't particularly like these helpers. Why can't the tests that
> care about the names specify the names?

Yeah, good catch; I got rid of them.

>
> > + def getLaunchpadlib(self):
> > + user = self.factory.makePerson()
> > + return launchpadlib_for("...

Revision history for this message
Guilherme Salgado (salgado) wrote :

I had to do a few other changes because Sprint currently doesn't
implement all of IHasSpecifications as it claims to do.

This became apparent because some code which gets a snapshot of a Sprint
tried to access .all_specifications as part of that. This probably
happened only now because the snapshotting code ignores Attribute fields
so it was ignoring that and other fields of IHasSpecifications.

These changes are in r11984

Revision history for this message
Guilherme Salgado (salgado) wrote :

And I'd also like to sneak this extra tiny change (r11985) to expose implementation_status, which will also be needed before we can make the work-item tracker use the API instead of screen scraping.

Revision history for this message
Michael Hudson-Doyle (mwhudson) wrote :
Download full text (14.1 KiB)

On Fri, 26 Nov 2010 13:11:21 -0000, Guilherme Salgado <email address hidden> wrote:
> Hi Michael,
>
> Thanks for the review!
>
> On Fri, 2010-11-26 at 01:12 +0000, Michael Hudson-Doyle wrote:
> > Review: Approve
> > Hi Salgado,
> >
> > Yay for progress on this issue. I entirely support the postponing of
> > all the difficult stuff :-)
> >
> > I have some quibbles about the tests, but nothing serious I hope.
>
> I like all your suggestions and since it's Friday and I'm all excited
> with the weekend approaching, I'm proposing even more changes. :)

Hah!

> > > === 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 22:50:47 +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)
> >
> > It's a shame that we don't have the app specific circular import files
> > yet... oh well.
>
> That'd be a slight improvement, but it's still a PITA to maintain these
> "patches".

Yeah. I saw what you had to do with SprintSpecification too :/

> > > === 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 22:50:47 +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.makeD...

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/canonical/launchpad/interfaces/_schema_circular_imports.py'
2--- lib/canonical/launchpad/interfaces/_schema_circular_imports.py 2010-11-17 22:18:34 +0000
3+++ lib/canonical/launchpad/interfaces/_schema_circular_imports.py 2010-11-26 18:08:22 +0000
4@@ -34,6 +34,10 @@
5 )
6 from lp.blueprints.interfaces.specification import ISpecification
7 from lp.blueprints.interfaces.specificationbranch import ISpecificationBranch
8+from lp.blueprints.interfaces.specificationtarget import (
9+ IHasSpecifications,
10+ ISpecificationTarget,
11+ )
12 from lp.bugs.interfaces.bug import (
13 IBug,
14 IFrontPageBugAddForm,
15@@ -516,3 +520,13 @@
16
17 # IProductSeries
18 patch_reference_property(IProductSeries, 'product', IProduct)
19+
20+# ISpecificationTarget
21+patch_entry_return_type(
22+ ISpecificationTarget, 'getSpecification', ISpecification)
23+
24+# IHasSpecifications
25+patch_collection_property(
26+ IHasSpecifications, 'all_specifications', ISpecification)
27+patch_collection_property(
28+ IHasSpecifications, 'valid_specifications', ISpecification)
29
30=== modified file 'lib/lp/app/doc/tales.txt'
31--- lib/lp/app/doc/tales.txt 2010-10-18 22:24:59 +0000
32+++ lib/lp/app/doc/tales.txt 2010-11-26 18:08:22 +0000
33@@ -673,8 +673,11 @@
34 Blueprints
35 ..........
36
37+ >>> from lp.blueprints.interfaces.specification import (
38+ ... SpecificationPriority)
39 >>> login('test@canonical.com')
40- >>> specification = factory.makeSpecification()
41+ >>> specification = factory.makeSpecification(
42+ ... priority=SpecificationPriority.UNDEFINED)
43 >>> test_tales("specification/fmt:link", specification=specification)
44 u'<a...class="sprite blueprint-undefined">...</a>'
45
46@@ -682,7 +685,8 @@
47 Blueprint branches
48 ..................
49
50- >>> specification = factory.makeSpecification()
51+ >>> specification = factory.makeSpecification(
52+ ... priority=SpecificationPriority.UNDEFINED)
53 >>> branch = factory.makeAnyBranch()
54 >>> specification_branch = specification.linkBranch(branch, branch.owner)
55 >>> test_tales("specification_branch/fmt:link",
56
57=== modified file 'lib/lp/blueprints/interfaces/specification.py'
58--- lib/lp/blueprints/interfaces/specification.py 2010-11-23 20:19:24 +0000
59+++ lib/lp/blueprints/interfaces/specification.py 2010-11-26 18:08:22 +0000
60@@ -19,7 +19,12 @@
61 ]
62
63
64-from lazr.restful.declarations import export_as_webservice_entry
65+from lazr.restful.declarations import (
66+ exported,
67+ export_as_webservice_entry,
68+ )
69+from lazr.restful.fields import ReferenceChoice
70+
71 from zope.component import getUtility
72 from zope.interface import (
73 Attribute,
74@@ -44,9 +49,13 @@
75 SpecificationLifecycleStatus,
76 SpecificationPriority,
77 )
78-from lp.blueprints.interfaces.specificationtarget import IHasSpecifications
79+from lp.blueprints.interfaces.specificationtarget import (
80+ IHasSpecifications,
81+ ISpecificationTarget,
82+ )
83 from lp.blueprints.interfaces.sprint import ISprint
84 from lp.code.interfaces.branchlink import IHasLinkedBranches
85+from lp.registry.interfaces.milestone import IMilestone
86 from lp.registry.interfaces.projectgroup import IProjectGroup
87 from lp.registry.interfaces.role import IHasOwner
88 from lp.services.fields import (
89@@ -119,48 +128,69 @@
90 class INewSpecification(Interface):
91 """A schema for a new specification."""
92
93- name = SpecNameField(
94- title=_('Name'), required=True, readonly=False,
95- description=_(
96- "May contain lower-case letters, numbers, and dashes. "
97- "It will be used in the specification url. "
98- "Examples: mozilla-type-ahead-find, postgres-smart-serial."))
99- title = Title(
100- title=_('Title'), required=True, description=_(
101- "Describe the feature as clearly as possible in up to 70 "
102- "characters. This title is displayed in every feature "
103- "list or report."))
104- specurl = SpecURLField(
105- title=_('Specification URL'), required=False,
106- description=_(
107- "The URL of the specification. This is usually a wiki page."),
108- constraint=valid_webref)
109- summary = Summary(
110- title=_('Summary'), required=True, description=_(
111- "A single-paragraph description of the feature. "
112- "This will also be displayed in most feature listings."))
113- definition_status = Choice(
114- title=_('Definition Status'),
115- vocabulary=SpecificationDefinitionStatus,
116- default=SpecificationDefinitionStatus.NEW,
117- description=_(
118- "The current status of the process to define the "
119- "feature and get approval for the implementation plan."))
120- assignee = PublicPersonChoice(
121- title=_('Assignee'), required=False,
122- description=_("The person responsible for implementing the feature."),
123- vocabulary='ValidPersonOrTeam')
124- drafter = PublicPersonChoice(
125- title=_('Drafter'), required=False,
126- description=_(
127- "The person responsible for drafting the specification."),
128- vocabulary='ValidPersonOrTeam')
129- approver = PublicPersonChoice(
130- title=_('Approver'), required=False,
131- description=_(
132- "The person responsible for approving the specification, "
133- "and for reviewing the code when it's ready to be landed."),
134- vocabulary='ValidPersonOrTeam')
135+ name = exported(
136+ SpecNameField(
137+ title=_('Name'), required=True, readonly=False,
138+ description=_(
139+ "May contain lower-case letters, numbers, and dashes. "
140+ "It will be used in the specification url. "
141+ "Examples: mozilla-type-ahead-find, postgres-smart-serial.")),
142+ ('devel', dict(exported=True)), exported=False)
143+ title = exported(
144+ Title(
145+ title=_('Title'), required=True, description=_(
146+ "Describe the feature as clearly as possible in up to 70 "
147+ "characters. This title is displayed in every feature "
148+ "list or report.")),
149+ ('devel', dict(exported=True)), exported=False)
150+ specurl = exported(
151+ SpecURLField(
152+ title=_('Specification URL'), required=False,
153+ description=_(
154+ "The URL of the specification. This is usually a wiki page."),
155+ constraint=valid_webref),
156+ ('devel', dict(exported=True, exported_as='specification_url')),
157+ exported=False)
158+ summary = exported(
159+ Summary(
160+ title=_('Summary'), required=True, description=_(
161+ "A single-paragraph description of the feature. "
162+ "This will also be displayed in most feature listings.")),
163+ ('devel', dict(exported=True)), exported=False)
164+ # XXX: salgado, 2010-11-25, bug=680880: We need a method for changing the
165+ # definition_status because when that happens we may need to call
166+ # updateLifecycleStatus().
167+ definition_status = exported(
168+ Choice(
169+ title=_('Definition Status'),
170+ vocabulary=SpecificationDefinitionStatus,
171+ default=SpecificationDefinitionStatus.NEW,
172+ description=_(
173+ "The current status of the process to define the "
174+ "feature and get approval for the implementation plan.")),
175+ ('devel', dict(exported=True, readonly=True)), exported=False)
176+ assignee = exported(
177+ PublicPersonChoice(
178+ title=_('Assignee'), required=False,
179+ description=_(
180+ "The person responsible for implementing the feature."),
181+ vocabulary='ValidPersonOrTeam'),
182+ ('devel', dict(exported=True)), exported=False)
183+ drafter = exported(
184+ PublicPersonChoice(
185+ title=_('Drafter'), required=False,
186+ description=_(
187+ "The person responsible for drafting the specification."),
188+ vocabulary='ValidPersonOrTeam'),
189+ ('devel', dict(exported=True)), exported=False)
190+ approver = exported(
191+ PublicPersonChoice(
192+ title=_('Approver'), required=False,
193+ description=_(
194+ "The person responsible for approving the specification, "
195+ "and for reviewing the code when it's ready to be landed."),
196+ vocabulary='ValidPersonOrTeam'),
197+ ('devel', dict(exported=True)), exported=False)
198
199
200 class INewSpecificationProjectTarget(Interface):
201@@ -201,10 +231,15 @@
202
203 Requires the user to specify a distribution or a product as a target.
204 """
205- target = Choice(title=_("For"),
206- description=_("The project for which this proposal is "
207- "being made."),
208- required=True, vocabulary='DistributionOrProduct')
209+ # Exported as readonly for simplicity, but could be exported as read-write
210+ # using setTarget() as the mutator.
211+ target = exported(
212+ ReferenceChoice(
213+ title=_('For'), required=True, vocabulary='DistributionOrProduct',
214+ description=_(
215+ "The project for which this proposal is being made."),
216+ schema=ISpecificationTarget),
217+ ('devel', dict(exported=True, readonly=True)), exported=False)
218
219
220 class ISpecificationEditRestricted(Interface):
221@@ -235,40 +270,54 @@
222 # referencing it.
223 id = Int(title=_("Database ID"), required=True, readonly=True)
224
225- priority = Choice(
226- title=_('Priority'), vocabulary=SpecificationPriority,
227- default=SpecificationPriority.UNDEFINED, required=True)
228- datecreated = Datetime(
229- title=_('Date Created'), required=True, readonly=True)
230- owner = PublicPersonChoice(
231- title=_('Owner'), required=True, readonly=True,
232- vocabulary='ValidPersonOrTeam')
233- # target
234+ priority = exported(
235+ Choice(
236+ title=_('Priority'), vocabulary=SpecificationPriority,
237+ default=SpecificationPriority.UNDEFINED, required=True),
238+ ('devel', dict(exported=True)), exported=False)
239+ datecreated = exported(
240+ Datetime(
241+ title=_('Date Created'), required=True, readonly=True),
242+ ('devel', dict(exported=True, exported_as='date_created')),
243+ exported=False)
244+ owner = exported(
245+ PublicPersonChoice(
246+ title=_('Owner'), required=True, readonly=True,
247+ vocabulary='ValidPersonOrTeam'),
248+ ('devel', dict(exported=True)), exported=False)
249+
250 product = Choice(title=_('Project'), required=False,
251- vocabulary='Product')
252+ vocabulary='Product')
253 distribution = Choice(title=_('Distribution'), required=False,
254- vocabulary='Distribution')
255+ vocabulary='Distribution')
256
257- # series
258- productseries = Choice(title=_('Series Goal'), required=False,
259+ productseries = Choice(
260+ title=_('Series Goal'), required=False,
261 vocabulary='FilteredProductSeries',
262 description=_(
263- "Choose a series in which you would like to deliver "
264- "this feature. Selecting '(no value)' will clear the goal."))
265- distroseries = Choice(title=_('Series Goal'), required=False,
266+ "Choose a series in which you would like to deliver "
267+ "this feature. Selecting '(no value)' will clear the goal."))
268+ distroseries = Choice(
269+ title=_('Series Goal'), required=False,
270 vocabulary='FilteredDistroSeries',
271 description=_(
272 "Choose a series in which you would like to deliver "
273 "this feature. Selecting '(no value)' will clear the goal."))
274
275 # milestone
276- milestone = Choice(
277- title=_('Milestone'), required=False, vocabulary='Milestone',
278- description=_(
279- "The milestone in which we would like this feature to be "
280- "delivered."))
281+ milestone = exported(
282+ ReferenceChoice(
283+ title=_('Milestone'), required=False, vocabulary='Milestone',
284+ description=_(
285+ "The milestone in which we would like this feature to be "
286+ "delivered."),
287+ schema=IMilestone),
288+ ('devel', dict(exported=True)), exported=False)
289
290 # nomination to a series for release management
291+ # XXX: It'd be nice to export goal as read-only, but it's tricky because
292+ # users will need to be aware of goalstatus as what's returned by .goal
293+ # may not be the accepted goal.
294 goal = Attribute("The series for which this feature is a goal.")
295 goalstatus = Choice(
296 title=_('Goal Acceptance'), vocabulary=SpecificationGoalStatus,
297@@ -283,10 +332,12 @@
298 date_goal_decided = Attribute("The date the spec was approved "
299 "or declined as a goal.")
300
301- whiteboard = Text(title=_('Status Whiteboard'), required=False,
302- description=_(
303- "Any notes on the status of this spec you would like to make. "
304- "Your changes will override the current text."))
305+ whiteboard = exported(
306+ Text(title=_('Status Whiteboard'), required=False,
307+ description=_(
308+ "Any notes on the status of this spec you would like to "
309+ "make. Your changes will override the current text.")),
310+ ('devel', dict(exported=True)), exported=False)
311 direction_approved = Bool(title=_('Basic direction approved?'),
312 required=False, default=False, description=_("Check this to "
313 "indicate that the drafter and assignee have satisfied the "
314@@ -297,11 +348,15 @@
315 "number of developer days it will take to implement this feature. "
316 "Please only provide an estimate if you are relatively confident "
317 "in the number."))
318- implementation_status = Choice(title=_("Implementation Status"),
319- required=True, default=SpecificationImplementationStatus.UNKNOWN,
320- vocabulary=SpecificationImplementationStatus, description=_(
321- "The state of progress being made on the actual implementation or "
322- "delivery of this feature."))
323+ implementation_status = exported(
324+ Choice(
325+ title=_("Implementation Status"), required=True,
326+ default=SpecificationImplementationStatus.UNKNOWN,
327+ vocabulary=SpecificationImplementationStatus,
328+ description=_(
329+ "The state of progress being made on the actual "
330+ "implementation or delivery of this feature.")),
331+ ('devel', dict(exported=True, readonly=True)), exported=False)
332 superseded_by = Choice(title=_("Superseded by"),
333 required=False, default=None,
334 vocabulary='Specification', description=_("The specification "
335
336=== modified file 'lib/lp/blueprints/interfaces/specificationtarget.py'
337--- lib/lp/blueprints/interfaces/specificationtarget.py 2010-08-20 20:31:18 +0000
338+++ lib/lp/blueprints/interfaces/specificationtarget.py 2010-11-26 18:08:22 +0000
339@@ -17,6 +17,22 @@
340 Attribute,
341 Interface,
342 )
343+from zope.schema import TextLine
344+
345+from lazr.restful.declarations import (
346+ exported,
347+ export_as_webservice_entry,
348+ export_read_operation,
349+ operation_for_version,
350+ operation_parameters,
351+ operation_returns_entry,
352+ )
353+from lazr.restful.fields import (
354+ CollectionField,
355+ Reference,
356+ )
357+
358+from canonical.launchpad import _
359
360
361 class IHasSpecifications(Interface):
362@@ -26,16 +42,30 @@
363 associated with them, and you can use this interface to query those.
364 """
365
366- all_specifications = Attribute(
367- 'A list of all specifications, regardless of status or approval '
368- 'or completion, for this object.')
369+ all_specifications = exported(
370+ CollectionField(
371+ title=_("All specifications"),
372+ value_type=Reference(schema=Interface), # ISpecification, really.
373+ readonly=True,
374+ description=_(
375+ 'A list of all specifications, regardless of status or '
376+ 'approval or completion, for this object.')),
377+ ('devel', dict(exported=True)), exported=False)
378
379 has_any_specifications = Attribute(
380 'A true or false indicator of whether or not this object has any '
381 'specifications associated with it, regardless of their status.')
382
383- valid_specifications = Attribute(
384- 'A list of all specifications that are not obsolete.')
385+ valid_specifications = exported(
386+ CollectionField(
387+ title=_("Valid specifications"),
388+ value_type=Reference(schema=Interface), # ISpecification, really.
389+ readonly=True,
390+ description=_(
391+ 'All specifications that are not obsolete. When called from '
392+ 'an ISpecificationGoal it will also exclude the ones that '
393+ 'have not been accepted for that goal')),
394+ ('devel', dict(exported=True)), exported=False)
395
396 latest_specifications = Attribute(
397 "The latest 5 specifications registered for this context.")
398@@ -63,12 +93,18 @@
399 """
400
401
402-
403 class ISpecificationTarget(IHasSpecifications):
404 """An interface for the objects which actually have unique
405 specifications directly attached to them.
406 """
407
408+ export_as_webservice_entry()
409+
410+ @operation_parameters(
411+ name=TextLine(title=_('The name of the specification')))
412+ @operation_returns_entry(Interface) # really ISpecification
413+ @export_read_operation()
414+ @operation_for_version('devel')
415 def getSpecification(name):
416 """Returns the specification with the given name, for this target,
417 or None.
418
419=== modified file 'lib/lp/blueprints/interfaces/webservice.py'
420--- lib/lp/blueprints/interfaces/webservice.py 2010-11-09 16:25:22 +0000
421+++ lib/lp/blueprints/interfaces/webservice.py 2010-11-26 18:08:22 +0000
422@@ -16,6 +16,7 @@
423
424 from lp.blueprints.interfaces.specification import ISpecification
425 from lp.blueprints.interfaces.specificationbranch import ISpecificationBranch
426+from lp.blueprints.interfaces.specificationtarget import ISpecificationTarget
427 # XXX: JonathanLange 2010-11-09 bug=673083: Legacy work-around for circular
428 # import bugs. Break this up into a per-package thing.
429 from canonical.launchpad.interfaces import _schema_circular_imports
430
431=== modified file 'lib/lp/blueprints/model/specification.py'
432--- lib/lp/blueprints/model/specification.py 2010-11-23 19:35:46 +0000
433+++ lib/lp/blueprints/model/specification.py 2010-11-26 18:08:22 +0000
434@@ -74,8 +74,6 @@
435 from lp.blueprints.model.specificationsubscription import (
436 SpecificationSubscription,
437 )
438-from lp.blueprints.model.sprint import Sprint
439-from lp.blueprints.model.sprintspecification import SprintSpecification
440 from lp.bugs.interfaces.buglink import IBugLinkTarget
441 from lp.bugs.model.buglinktarget import BugLinkTargetMixin
442 from lp.registry.interfaces.distribution import IDistribution
443@@ -563,6 +561,8 @@
444 # sprint linking
445 def linkSprint(self, sprint, user):
446 """See ISpecification."""
447+ from lp.blueprints.model.sprintspecification import (
448+ SprintSpecification)
449 for sprint_link in self.sprint_links:
450 # sprints have unique names
451 if sprint_link.sprint.name == sprint.name:
452@@ -575,6 +575,8 @@
453
454 def unlinkSprint(self, sprint):
455 """See ISpecification."""
456+ from lp.blueprints.model.sprintspecification import (
457+ SprintSpecification)
458 for sprint_link in self.sprint_links:
459 # sprints have unique names
460 if sprint_link.sprint.name == sprint.name:
461@@ -889,6 +891,7 @@
462 @property
463 def coming_sprints(self):
464 """See ISpecificationSet."""
465+ from lp.blueprints.model.sprint import Sprint
466 return Sprint.select("time_ends > 'NOW'", orderBy='time_starts',
467 limit=5)
468
469
470=== modified file 'lib/lp/blueprints/model/sprint.py'
471--- lib/lp/blueprints/model/sprint.py 2010-11-24 11:12:51 +0000
472+++ lib/lp/blueprints/model/sprint.py 2010-11-26 18:08:22 +0000
473@@ -42,13 +42,14 @@
474 ISprint,
475 ISprintSet,
476 )
477+from lp.blueprints.model.specification import HasSpecificationsMixin
478 from lp.blueprints.model.sprintattendance import SprintAttendance
479 from lp.blueprints.model.sprintspecification import SprintSpecification
480 from lp.registry.interfaces.person import validate_public_person
481 from lp.registry.model.hasdrivers import HasDriversMixin
482
483
484-class Sprint(SQLBase, HasDriversMixin):
485+class Sprint(SQLBase, HasDriversMixin, HasSpecificationsMixin):
486 """See `ISprint`."""
487
488 implements(ISprint, IHasLogo, IHasMugshot, IHasIcon)
489@@ -93,7 +94,7 @@
490 """See IHasDrivers."""
491 if self.driver is not None:
492 return [self.driver, self.owner]
493- return [self.owner,]
494+ return [self.owner]
495
496 # useful joins
497 attendees = SQLRelatedJoin('Person',
498@@ -141,7 +142,7 @@
499
500 # filter based on completion. see the implementation of
501 # Specification.is_complete() for more details
502- completeness = Specification.completeness_clause
503+ completeness = Specification.completeness_clause
504
505 if SpecificationFilter.COMPLETE in filter:
506 query += ' AND ( %s ) ' % completeness
507
508=== modified file 'lib/lp/blueprints/stories/standalone/sprint-links.txt'
509--- lib/lp/blueprints/stories/standalone/sprint-links.txt 2009-09-22 10:48:09 +0000
510+++ lib/lp/blueprints/stories/standalone/sprint-links.txt 2010-11-26 18:08:22 +0000
511@@ -13,10 +13,6 @@
512 >>> browser.open('http://blueprints.launchpad.dev/firefox/+spec/canvas')
513 >>> browser.isHtml
514 True
515- >>> 'Accepted' in browser.contents # make sure the page is not polluted
516- False
517- >>> 'Proposed' in browser.contents # make sure the page is not polluted
518- False
519
520 Then we are going to propose it for the meeting agenda:
521
522
523=== added file 'lib/lp/blueprints/tests/test_implements.py'
524--- lib/lp/blueprints/tests/test_implements.py 1970-01-01 00:00:00 +0000
525+++ lib/lp/blueprints/tests/test_implements.py 2010-11-26 18:08:22 +0000
526@@ -0,0 +1,65 @@
527+# Copyright 2009 Canonical Ltd. This software is licensed under the
528+# GNU Affero General Public License version 3 (see the file LICENSE).
529+
530+"""Tests that various objects implement specification-related interfaces."""
531+
532+__metaclass__ = type
533+
534+from canonical.testing import DatabaseFunctionalLayer
535+from lp.blueprints.interfaces.specificationtarget import (
536+ IHasSpecifications, ISpecificationTarget)
537+from lp.testing import TestCaseWithFactory
538+
539+
540+class ImplementsIHasSpecificationsTests(TestCaseWithFactory):
541+ """Test that various objects implement IHasSpecifications."""
542+ layer = DatabaseFunctionalLayer
543+
544+ def test_product_implements_IHasSpecifications(self):
545+ product = self.factory.makeProduct()
546+ self.assertProvides(product, IHasSpecifications)
547+
548+ def test_distribution_implements_IHasSpecifications(self):
549+ product = self.factory.makeProduct()
550+ self.assertProvides(product, IHasSpecifications)
551+
552+ def test_projectgroup_implements_IHasSpecifications(self):
553+ projectgroup = self.factory.makeProject()
554+ self.assertProvides(projectgroup, IHasSpecifications)
555+
556+ def test_person_implements_IHasSpecifications(self):
557+ person = self.factory.makePerson()
558+ self.assertProvides(person, IHasSpecifications)
559+
560+ def test_productseries_implements_IHasSpecifications(self):
561+ productseries = self.factory.makeProductSeries()
562+ self.assertProvides(productseries, IHasSpecifications)
563+
564+ def test_distroseries_implements_IHasSpecifications(self):
565+ distroseries = self.factory.makeDistroSeries()
566+ self.assertProvides(distroseries, IHasSpecifications)
567+
568+ def test_sprint_implements_IHasSpecifications(self):
569+ sprint = self.factory.makeSprint()
570+ self.assertProvides(sprint, IHasSpecifications)
571+
572+
573+class ImplementsISpecificationTargetTests(TestCaseWithFactory):
574+ """Test that various objects implement ISpecificationTarget."""
575+ layer = DatabaseFunctionalLayer
576+
577+ def test_product_implements_ISpecificationTarget(self):
578+ product = self.factory.makeProduct()
579+ self.assertProvides(product, ISpecificationTarget)
580+
581+ def test_distribution_implements_ISpecificationTarget(self):
582+ product = self.factory.makeProduct()
583+ self.assertProvides(product, ISpecificationTarget)
584+
585+ def test_productseries_implements_ISpecificationTarget(self):
586+ productseries = self.factory.makeProductSeries()
587+ self.assertProvides(productseries, ISpecificationTarget)
588+
589+ def test_distroseries_implements_ISpecificationTarget(self):
590+ distroseries = self.factory.makeDistroSeries()
591+ self.assertProvides(distroseries, ISpecificationTarget)
592
593=== added file 'lib/lp/blueprints/tests/test_webservice.py'
594--- lib/lp/blueprints/tests/test_webservice.py 1970-01-01 00:00:00 +0000
595+++ lib/lp/blueprints/tests/test_webservice.py 2010-11-26 18:08:22 +0000
596@@ -0,0 +1,395 @@
597+# Copyright 2009 Canonical Ltd. This software is licensed under the
598+# GNU Affero General Public License version 3 (see the file LICENSE).
599+
600+"""Webservice unit tests related to Launchpad blueprints."""
601+
602+__metaclass__ = type
603+
604+from canonical.testing import DatabaseFunctionalLayer
605+from canonical.launchpad.testing.pages import webservice_for_person
606+from lp.blueprints.interfaces.specification import (
607+ SpecificationDefinitionStatus,
608+ )
609+from lp.testing import (
610+ launchpadlib_for, TestCaseWithFactory)
611+
612+
613+class SpecificationWebserviceTestCase(TestCaseWithFactory):
614+
615+ def getLaunchpadlib(self):
616+ user = self.factory.makePerson()
617+ return launchpadlib_for("testing", user, version='devel')
618+
619+ def getSpecOnWebservice(self, spec_object):
620+ launchpadlib = self.getLaunchpadlib()
621+ return launchpadlib.load(
622+ '/%s/+spec/%s' % (spec_object.target.name, spec_object.name))
623+
624+ def getPillarOnWebservice(self, pillar_obj):
625+ # XXX: 2010-11-26, salgado, bug=681767: Can't use relative URLs here.
626+ launchpadlib = self.getLaunchpadlib()
627+ return launchpadlib.load(
628+ str(launchpadlib._root_uri) + '/' + pillar_obj.name)
629+
630+
631+class SpecificationAttributeWebserviceTests(SpecificationWebserviceTestCase):
632+ """Test accessing specification attributes over the webservice."""
633+ layer = DatabaseFunctionalLayer
634+
635+ def test_representation_is_empty_on_1_dot_0(self):
636+ # ISpecification is exposed on the 1.0 version so that they can be
637+ # linked against branches, but none of its fields is exposed on that
638+ # version as we expect it to undergo significant refactorings before
639+ # it's ready for prime time.
640+ spec = self.factory.makeSpecification()
641+ user = self.factory.makePerson()
642+ webservice = webservice_for_person(user)
643+ response = webservice.get(
644+ '/%s/+spec/%s' % (spec.product.name, spec.name))
645+ expected_keys = sorted(
646+ [u'self_link', u'http_etag', u'resource_type_link'])
647+ self.assertEqual(response.status, 200)
648+ self.assertEqual(sorted(response.jsonBody().keys()), expected_keys)
649+
650+ def test_representation_contains_name(self):
651+ spec = self.factory.makeSpecification()
652+ spec_webservice = self.getSpecOnWebservice(spec)
653+ self.assertEqual(spec.name, spec_webservice.name)
654+
655+ def test_representation_contains_target(self):
656+ spec = self.factory.makeSpecification(
657+ product=self.factory.makeProduct())
658+ spec_webservice = self.getSpecOnWebservice(spec)
659+ self.assertEqual(spec.target.name, spec_webservice.target.name)
660+
661+ def test_representation_contains_title(self):
662+ spec = self.factory.makeSpecification(title='Foo')
663+ spec_webservice = self.getSpecOnWebservice(spec)
664+ self.assertEqual(spec.title, spec_webservice.title)
665+
666+ def test_representation_contains_specification_url(self):
667+ spec = self.factory.makeSpecification(specurl='http://example.com')
668+ spec_webservice = self.getSpecOnWebservice(spec)
669+ self.assertEqual(spec.specurl, spec_webservice.specification_url)
670+
671+ def test_representation_contains_summary(self):
672+ spec = self.factory.makeSpecification(summary='Foo')
673+ spec_webservice = self.getSpecOnWebservice(spec)
674+ self.assertEqual(spec.summary, spec_webservice.summary)
675+
676+ def test_representation_contains_implementation_status(self):
677+ spec = self.factory.makeSpecification()
678+ spec_webservice = self.getSpecOnWebservice(spec)
679+ self.assertEqual(
680+ spec.implementation_status.title,
681+ spec_webservice.implementation_status)
682+
683+ def test_representation_contains_definition_status(self):
684+ spec = self.factory.makeSpecification()
685+ spec_webservice = self.getSpecOnWebservice(spec)
686+ self.assertEqual(
687+ spec.definition_status.title, spec_webservice.definition_status)
688+
689+ def test_representation_contains_assignee(self):
690+ # Hard-code the person's name or else we'd need to set up a zope
691+ # interaction as IPerson.name is protected.
692+ spec = self.factory.makeSpecification(
693+ assignee=self.factory.makePerson(name='test-person'))
694+ spec_webservice = self.getSpecOnWebservice(spec)
695+ self.assertEqual('test-person', spec_webservice.assignee.name)
696+
697+ def test_representation_contains_drafter(self):
698+ spec = self.factory.makeSpecification(
699+ drafter=self.factory.makePerson(name='test-person'))
700+ spec_webservice = self.getSpecOnWebservice(spec)
701+ self.assertEqual('test-person', spec_webservice.drafter.name)
702+
703+ def test_representation_contains_approver(self):
704+ spec = self.factory.makeSpecification(
705+ approver=self.factory.makePerson(name='test-person'))
706+ spec_webservice = self.getSpecOnWebservice(spec)
707+ self.assertEqual('test-person', spec_webservice.approver.name)
708+
709+ def test_representation_contains_owner(self):
710+ spec = self.factory.makeSpecification(
711+ owner=self.factory.makePerson(name='test-person'))
712+ spec_webservice = self.getSpecOnWebservice(spec)
713+ self.assertEqual('test-person', spec_webservice.owner.name)
714+
715+ def test_representation_contains_priority(self):
716+ spec = self.factory.makeSpecification()
717+ spec_webservice = self.getSpecOnWebservice(spec)
718+ self.assertEqual(spec.priority.title, spec_webservice.priority)
719+
720+ def test_representation_contains_date_created(self):
721+ spec = self.factory.makeSpecification()
722+ spec_webservice = self.getSpecOnWebservice(spec)
723+ self.assertEqual(spec.datecreated, spec_webservice.date_created)
724+
725+ def test_representation_contains_whiteboard(self):
726+ spec = self.factory.makeSpecification(whiteboard='Test')
727+ spec_webservice = self.getSpecOnWebservice(spec)
728+ self.assertEqual(spec.whiteboard, spec_webservice.whiteboard)
729+
730+ def test_representation_contains_milestone(self):
731+ product = self.factory.makeProduct()
732+ productseries = self.factory.makeProductSeries(product=product)
733+ milestone = self.factory.makeMilestone(
734+ name="1.0", product=product, productseries=productseries)
735+ spec_object = self.factory.makeSpecification(
736+ product=product, goal=productseries, milestone=milestone)
737+ spec = self.getSpecOnWebservice(spec_object)
738+ self.assertEqual("1.0", spec.milestone.name)
739+
740+
741+class SpecificationTargetTests(SpecificationWebserviceTestCase):
742+ """Tests for accessing specifications via their targets."""
743+ layer = DatabaseFunctionalLayer
744+
745+ def test_get_specification_on_product(self):
746+ product = self.factory.makeProduct(name="fooix")
747+ spec_object = self.factory.makeSpecification(
748+ product=product, name="some-spec")
749+ product_on_webservice = self.getPillarOnWebservice(product)
750+ spec = product_on_webservice.getSpecification(name="some-spec")
751+ self.assertEqual("some-spec", spec.name)
752+ self.assertEqual("fooix", spec.target.name)
753+
754+ def test_get_specification_on_distribution(self):
755+ distribution = self.factory.makeDistribution(name="foobuntu")
756+ spec_object = self.factory.makeSpecification(
757+ distribution=distribution, name="some-spec")
758+ distro_on_webservice = self.getPillarOnWebservice(distribution)
759+ spec = distro_on_webservice.getSpecification(name="some-spec")
760+ self.assertEqual("some-spec", spec.name)
761+ self.assertEqual("foobuntu", spec.target.name)
762+
763+ def test_get_specification_on_productseries(self):
764+ product = self.factory.makeProduct(name="fooix")
765+ productseries = self.factory.makeProductSeries(
766+ product=product, name="fooix-dev")
767+ spec_object = self.factory.makeSpecification(
768+ product=product, name="some-spec", goal=productseries)
769+ product_on_webservice = self.getPillarOnWebservice(product)
770+ productseries_on_webservice = product_on_webservice.getSeries(
771+ name="fooix-dev")
772+ spec = productseries_on_webservice.getSpecification(name="some-spec")
773+ self.assertEqual("some-spec", spec.name)
774+ self.assertEqual("fooix", spec.target.name)
775+
776+ def test_get_specification_on_distroseries(self):
777+ distribution = self.factory.makeDistribution(name="foobuntu")
778+ distroseries = self.factory.makeDistroSeries(
779+ distribution=distribution, name="maudlin")
780+ spec_object = self.factory.makeSpecification(
781+ distribution=distribution, name="some-spec",
782+ goal=distroseries)
783+ distro_on_webservice = self.getPillarOnWebservice(distribution)
784+ distroseries_on_webservice = distro_on_webservice.getSeries(
785+ name_or_version="maudlin")
786+ spec = distroseries_on_webservice.getSpecification(name="some-spec")
787+ self.assertEqual("some-spec", spec.name)
788+ self.assertEqual("foobuntu", spec.target.name)
789+
790+ def test_get_specification_not_found(self):
791+ product = self.factory.makeProduct()
792+ product_on_webservice = self.getPillarOnWebservice(product)
793+ spec = product_on_webservice.getSpecification(name="nonexistant")
794+ self.assertEqual(None, spec)
795+
796+
797+class IHasSpecificationsTests(SpecificationWebserviceTestCase):
798+ """Tests for accessing IHasSpecifications methods over the webservice."""
799+ layer = DatabaseFunctionalLayer
800+
801+ def assertNamesOfSpecificationsAre(self, expected_names, specifications):
802+ names = [s.name for s in specifications]
803+ self.assertEqual(sorted(expected_names), sorted(names))
804+
805+ def test_product_all_specifications(self):
806+ product = self.factory.makeProduct()
807+ self.factory.makeSpecification(product=product, name="spec1")
808+ self.factory.makeSpecification(product=product, name="spec2")
809+ product_on_webservice = self.getPillarOnWebservice(product)
810+ self.assertNamesOfSpecificationsAre(
811+ ["spec1", "spec2"], product_on_webservice.all_specifications)
812+
813+ def test_product_valid_specifications(self):
814+ product = self.factory.makeProduct()
815+ self.factory.makeSpecification(product=product, name="spec1")
816+ self.factory.makeSpecification(
817+ product=product, name="spec2",
818+ status=SpecificationDefinitionStatus.OBSOLETE)
819+ product_on_webservice = self.getPillarOnWebservice(product)
820+ self.assertNamesOfSpecificationsAre(
821+ ["spec1"], product_on_webservice.valid_specifications)
822+
823+ def test_distribution_all_specifications(self):
824+ distribution = self.factory.makeDistribution()
825+ self.factory.makeSpecification(
826+ distribution=distribution, name="spec1")
827+ self.factory.makeSpecification(
828+ distribution=distribution, name="spec2")
829+ distro_on_webservice = self.getPillarOnWebservice(distribution)
830+ self.assertNamesOfSpecificationsAre(
831+ ["spec1", "spec2"], distro_on_webservice.all_specifications)
832+
833+ def test_distribution_valid_specifications(self):
834+ distribution = self.factory.makeDistribution()
835+ self.factory.makeSpecification(
836+ distribution=distribution, name="spec1")
837+ self.factory.makeSpecification(
838+ distribution=distribution, name="spec2",
839+ status=SpecificationDefinitionStatus.OBSOLETE)
840+ distro_on_webservice = self.getPillarOnWebservice(distribution)
841+ self.assertNamesOfSpecificationsAre(
842+ ["spec1"], distro_on_webservice.valid_specifications)
843+
844+ def test_distroseries_all_specifications(self):
845+ distribution = self.factory.makeDistribution()
846+ distroseries = self.factory.makeDistroSeries(
847+ name='maudlin', distribution=distribution)
848+ self.factory.makeSpecification(
849+ distribution=distribution, name="spec1",
850+ goal=distroseries)
851+ self.factory.makeSpecification(
852+ distribution=distribution, name="spec2",
853+ goal=distroseries)
854+ self.factory.makeSpecification(
855+ distribution=distribution, name="spec3")
856+ distro_on_webservice = self.getPillarOnWebservice(distribution)
857+ distroseries_on_webservice = distro_on_webservice.getSeries(
858+ name_or_version="maudlin")
859+ self.assertNamesOfSpecificationsAre(
860+ ["spec1", "spec2"],
861+ distroseries_on_webservice.all_specifications)
862+
863+ # XXX: salgado, 2010-11-25, bug=681432: Test disabled because
864+ # DistroSeries.valid_specifications is broken.
865+ def disabled_test_distroseries_valid_specifications(self):
866+ distribution = self.factory.makeDistribution()
867+ distroseries = self.factory.makeDistroSeries(
868+ name='maudlin', distribution=distribution)
869+ self.factory.makeSpecification(
870+ distribution=distribution, name="spec1",
871+ goal=distroseries)
872+ self.factory.makeSpecification(
873+ distribution=distribution, name="spec2",
874+ goal=distroseries)
875+ self.factory.makeSpecification(
876+ distribution=distribution, name="spec3",
877+ goal=distroseries,
878+ status=SpecificationDefinitionStatus.OBSOLETE)
879+ self.factory.makeSpecification(
880+ distribution=distribution, name="spec4")
881+ distro_on_webservice = self.getPillarOnWebservice(distribution)
882+ distroseries_on_webservice = distro_on_webservice.getSeries(
883+ name_or_version="maudlin")
884+ self.assertNamesOfSpecificationsAre(
885+ ["spec1", "spec2"],
886+ distroseries_on_webservice.valid_specifications)
887+
888+ def test_productseries_all_specifications(self):
889+ product = self.factory.makeProduct()
890+ productseries = self.factory.makeProductSeries(
891+ product=product, name="fooix-dev")
892+ self.factory.makeSpecification(
893+ product=product, name="spec1", goal=productseries)
894+ self.factory.makeSpecification(
895+ product=product, name="spec2", goal=productseries)
896+ self.factory.makeSpecification(product=product, name="spec3")
897+ product_on_webservice = self.getPillarOnWebservice(product)
898+ series_on_webservice = product_on_webservice.getSeries(
899+ name="fooix-dev")
900+ self.assertNamesOfSpecificationsAre(
901+ ["spec1", "spec2"], series_on_webservice.all_specifications)
902+
903+ def test_productseries_valid_specifications(self):
904+ product = self.factory.makeProduct()
905+ productseries = self.factory.makeProductSeries(
906+ product=product, name="fooix-dev")
907+ self.factory.makeSpecification(
908+ product=product, name="spec1", goal=productseries)
909+ self.factory.makeSpecification(
910+ product=product, name="spec2", goal=productseries)
911+ self.factory.makeSpecification(
912+ product=product, name="spec3", goal=productseries,
913+ status=SpecificationDefinitionStatus.OBSOLETE)
914+ self.factory.makeSpecification(product=product, name="spec4")
915+ product_on_webservice = self.getPillarOnWebservice(product)
916+ series_on_webservice = product_on_webservice.getSeries(
917+ name="fooix-dev")
918+ # Should this be different to the results for distroseries?
919+ self.assertNamesOfSpecificationsAre(
920+ ["spec1", "spec2"],
921+ series_on_webservice.valid_specifications)
922+
923+ def test_projectgroup_all_specifications(self):
924+ projectgroup = self.factory.makeProject()
925+ other_projectgroup = self.factory.makeProject()
926+ product1 = self.factory.makeProduct(project=projectgroup)
927+ product2 = self.factory.makeProduct(project=projectgroup)
928+ product3 = self.factory.makeProduct(project=other_projectgroup)
929+ self.factory.makeSpecification(
930+ product=product1, name="spec1")
931+ self.factory.makeSpecification(
932+ product=product2, name="spec2",
933+ status=SpecificationDefinitionStatus.OBSOLETE)
934+ self.factory.makeSpecification(
935+ product=product3, name="spec3")
936+ projectgroup_on_webservice = self.getPillarOnWebservice(projectgroup)
937+ # Should this be different to the results for distroseries?
938+ self.assertNamesOfSpecificationsAre(
939+ ["spec1", "spec2"],
940+ projectgroup_on_webservice.all_specifications)
941+
942+ def test_projectgroup_valid_specifications(self):
943+ projectgroup = self.factory.makeProject()
944+ other_projectgroup = self.factory.makeProject()
945+ product1 = self.factory.makeProduct(project=projectgroup)
946+ product2 = self.factory.makeProduct(project=projectgroup)
947+ product3 = self.factory.makeProduct(project=other_projectgroup)
948+ self.factory.makeSpecification(
949+ product=product1, name="spec1")
950+ self.factory.makeSpecification(
951+ product=product2, name="spec2",
952+ status=SpecificationDefinitionStatus.OBSOLETE)
953+ self.factory.makeSpecification(
954+ product=product3, name="spec3")
955+ projectgroup_on_webservice = self.getPillarOnWebservice(projectgroup)
956+ # Should this be different to the results for distroseries?
957+ self.assertNamesOfSpecificationsAre(
958+ ["spec1", "spec2"],
959+ projectgroup_on_webservice.valid_specifications)
960+
961+ def test_person_all_specifications(self):
962+ person = self.factory.makePerson(name="james-w")
963+ product = self.factory.makeProduct()
964+ self.factory.makeSpecification(
965+ product=product, name="spec1", drafter=person)
966+ self.factory.makeSpecification(
967+ product=product, name="spec2", approver=person,
968+ status=SpecificationDefinitionStatus.OBSOLETE)
969+ self.factory.makeSpecification(
970+ product=product, name="spec3")
971+ launchpadlib = self.getLaunchpadlib()
972+ person_on_webservice = launchpadlib.load(
973+ str(launchpadlib._root_uri) + '/~james-w')
974+ self.assertNamesOfSpecificationsAre(
975+ ["spec1", "spec2"], person_on_webservice.all_specifications)
976+
977+ def test_person_valid_specifications(self):
978+ person = self.factory.makePerson(name="james-w")
979+ product = self.factory.makeProduct()
980+ self.factory.makeSpecification(
981+ product=product, name="spec1", drafter=person)
982+ self.factory.makeSpecification(
983+ product=product, name="spec2", approver=person,
984+ status=SpecificationDefinitionStatus.OBSOLETE)
985+ self.factory.makeSpecification(
986+ product=product, name="spec3")
987+ launchpadlib = self.getLaunchpadlib()
988+ person_on_webservice = launchpadlib.load(
989+ str(launchpadlib._root_uri) + '/~james-w')
990+ self.assertNamesOfSpecificationsAre(
991+ ["spec1"], person_on_webservice.valid_specifications)
992
993=== modified file 'lib/lp/testing/factory.py'
994--- lib/lp/testing/factory.py 2010-11-18 12:05:34 +0000
995+++ lib/lp/testing/factory.py 2010-11-26 18:08:22 +0000
996@@ -102,7 +102,11 @@
997 from lp.app.enums import ServiceUsage
998 from lp.archiveuploader.dscfile import DSCFile
999 from lp.archiveuploader.uploadpolicy import BuildDaemonUploadPolicy
1000-from lp.blueprints.enums import SpecificationDefinitionStatus
1001+from lp.blueprints.enums import (
1002+ SpecificationDefinitionStatus,
1003+ SpecificationGoalStatus,
1004+ SpecificationPriority,
1005+ )
1006 from lp.blueprints.interfaces.specification import ISpecificationSet
1007 from lp.blueprints.interfaces.sprint import ISprintSet
1008 from lp.bugs.interfaces.bug import (
1009@@ -1671,7 +1675,9 @@
1010 def makeSpecification(self, product=None, title=None, distribution=None,
1011 name=None, summary=None, owner=None,
1012 status=SpecificationDefinitionStatus.NEW,
1013- implementation_status=None):
1014+ implementation_status=None, goal=None, specurl=None,
1015+ assignee=None, drafter=None, approver=None,
1016+ priority=None, whiteboard=None, milestone=None):
1017 """Create and return a new, arbitrary Blueprint.
1018
1019 :param product: The product to make the blueprint on. If one is
1020@@ -1687,17 +1693,32 @@
1021 title = self.getUniqueString('title')
1022 if owner is None:
1023 owner = self.makePerson()
1024+ if priority is None:
1025+ priority = SpecificationPriority.UNDEFINED
1026 spec = getUtility(ISpecificationSet).new(
1027 name=name,
1028 title=title,
1029 specurl=None,
1030 summary=summary,
1031 definition_status=status,
1032+ whiteboard=whiteboard,
1033 owner=owner,
1034+ assignee=assignee,
1035+ drafter=drafter,
1036+ approver=approver,
1037 product=product,
1038- distribution=distribution)
1039+ distribution=distribution,
1040+ priority=priority)
1041+ naked_spec = removeSecurityProxy(spec)
1042+ if status == SpecificationDefinitionStatus.OBSOLETE:
1043+ # This is to satisfy a DB constraint of obsolete specs.
1044+ naked_spec.completer = owner
1045+ naked_spec.date_completed = datetime.now(pytz.UTC)
1046+ naked_spec.specurl = specurl
1047+ naked_spec.milestone = milestone
1048+ if goal is not None:
1049+ naked_spec.proposeGoal(goal, spec.target.owner)
1050 if implementation_status is not None:
1051- naked_spec = removeSecurityProxy(spec)
1052 naked_spec.implementation_status = implementation_status
1053 naked_spec.updateLifecycleStatus(owner)
1054 return spec