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

Proposed by James Westby
Status: Merged
Merged at revision: 11997
Proposed branch: lp:~james-w/launchpad/expose-blueprints
Merge into: lp:launchpad
Diff against target: 1214 lines (+857/-115)
9 files modified
lib/canonical/launchpad/doc/tales.txt (+6/-2)
lib/canonical/launchpad/interfaces/_schema_circular_imports.py (+12/-0)
lib/lp/blueprints/interfaces/specification.py (+129/-96)
lib/lp/blueprints/interfaces/specificationtarget.py (+19/-0)
lib/lp/blueprints/model/specification.py (+24/-5)
lib/lp/blueprints/stories/standalone/sprint-links.txt (+0/-4)
lib/lp/blueprints/tests/test_implements.py (+61/-0)
lib/lp/blueprints/tests/test_webservice.py (+550/-0)
lib/lp/testing/factory.py (+56/-8)
To merge this branch: bzr merge lp:~james-w/launchpad/expose-blueprints
Reviewer Review Type Date Requested Status
Curtis Hovey (community) curtis Needs Fixing
Leonard Richardson (community) Needs Information
Review via email: mp+30026@code.launchpad.net

Commit message

Blueprints now have a basic API exposed, as "specifications".

Description of the change

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

./lib/lp/blueprints/model/specification.py
     336: E222 multiple spaces after operator
     375: E222 multiple spaces after operator
     681: E231 missing whitespace after ','
./lib/lp/blueprints/stories/standalone/sprint-links.txt
      12: source has bad indentation.
      19: source has bad indentation.
      32: source has bad indentation.
      41: source has bad indentation.
      65: source exceeds 78 characters.
      65: source has bad indentation.
      81: source has bad indentation.
      86: source has bad indentation.
      98: source has bad indentation.
     105: source has bad indentation.
./lib/lp/testing/factory.py
     824: E231 missing whitespace after ','
    1823: E231 missing whitespace after ','
    1946: E301 expected 1 blank line, found 0

None of which I am inclined to fix, as they are not in code that I have
changed or would require updating too much of the file to be worthwhile.

To post a comment you must log in.
Revision history for this message
Leonard Richardson (leonardr) wrote :
Download full text (5.0 KiB)

Thanks a lot for this branch. This is a branch I was planning to create as a way of introducing Benji to the web service, but instead we went through your branch and reviewed it.

I don't have any opinion on the whiteboard thing, but I'll make sure someone with an opinion looks at this branch.

= Questions about the exported objects =

1. Obviously you don't need to export everything to make this branch
useful, but I wonder why you exported ISpecification.goalstatus but
not ISpecification.goal.

2. "I added two new methods to IHasSpecifications, as if we exposed
the existing attributes we would serve the collections when just
getting the specification." Can you explain? There are many places
where we publish a collection associated with an entry. (Just within
IPerson we have 'languages', 'gpg_keys', 'pending_gpg_keys',
'wiki_names', 'irc_nicknames', etc.) Just getting the person doesn't
give you these collections--it gives you links to the collections.

3. Rather than publishing updateLifecycleStatus as a named operation, I
think you could publish it as the mutator for the 'completer' field. I
know that you're not publishing 'completer' right now, but I imagine
it will be published eventually, and publishing it now with a mutator
means that end-users will have to learn one less strangely-named named
operation.

The only catch is that updateLifecycleStatus() returns None if the
lifecycle is not in a state for being completed. It would be better to
return a 400 response code, which probably means raising a certain
exception. (I'm fuzzy on the details but I can help you with this when
the time comes.) We'd write a new method just for use in the web
service, which wrapped an updateLifecycleStatus() call with
appropriate exception-raising code.

(At the very least, call this operation something like 'complete', and
pass in the REQUEST_USER automatically--I don't think it makes much
sense to complete a blueprint on someone else's behalf.)

= Questions about the tests =

1. You changed the default priority for a specification created through
the factory to LOW, and then changed some tests that were assuming a
factory-created specification started out UNDEFINED. Is it easy to
leave the earlier behavior alone? Or are specifications with UNDEFINED
priority considered invalid, or don't show up in lists, or anything else?

2. You changed the factory's makeSpecification method to take a lot of
new arguments, which is fine, but you also changed
ISpecificationSet.new() to take the same arguments, which seems like
overkill. Can makeSpecification create a generic specification object
and then customize it?

3. Due to limitations of launchpadlib and the web service there's no
way around constructing the URL to a product when loading it in
launchpadlib. But, once you have that URL, is it possible to navigate
to a specific spec through launchpadlib object traversal? If so, I'd
like to see this in the test. If not, I think this branch should
publish some way of getting from an IHasSpecification to an
ISpecification, given the name--probably as a named operation.

4. In IHasSpecificationsTests, you could save a lot of setup code by
writing a helper methods that ta...

Read more...

review: Needs Information
Revision history for this message
James Westby (james-w) wrote :
Download full text (7.3 KiB)

On Fri, 16 Jul 2010 13:05:52 -0000, Leonard Richardson <email address hidden> wrote:
> Review: Needs Information
> Thanks a lot for this branch. This is a branch I was planning to create as a way of introducing Benji to the web service, but instead we went through your branch and reviewed it.
>
> I don't have any opinion on the whiteboard thing, but I'll make sure someone with an opinion looks at this branch.
>
> = Questions about the exported objects =
>
> 1. Obviously you don't need to export everything to make this branch
> useful, but I wonder why you exported ISpecification.goalstatus but
> not ISpecification.goal.

That was something I picked up from the work I merged from ajmitch. goal
is a bit more work as it is not a simple attribute, so I didn't bother
working on that, I just left goalstatus exported for now.

> 2. "I added two new methods to IHasSpecifications, as if we exposed
> the existing attributes we would serve the collections when just
> getting the specification." Can you explain? There are many places
> where we publish a collection associated with an entry. (Just within
> IPerson we have 'languages', 'gpg_keys', 'pending_gpg_keys',
> 'wiki_names', 'irc_nicknames', etc.) Just getting the person doesn't
> give you these collections--it gives you links to the collections.

Ok, I still think methods are more appropriate for this given parallels
with e.g. searchTasks, but I can change if you feel strongly.

Also, doesn't exposing collection attributes require them to have a
traversal? I don't think these do, as there is just /~person/+specs that
takes query arguments.

> 3. Rather than publishing updateLifecycleStatus as a named operation, I
> think you could publish it as the mutator for the 'completer' field. I
> know that you're not publishing 'completer' right now, but I imagine
> it will be published eventually, and publishing it now with a mutator
> means that end-users will have to learn one less strangely-named named
> operation.
>
> The only catch is that updateLifecycleStatus() returns None if the
> lifecycle is not in a state for being completed. It would be better to
> return a 400 response code, which probably means raising a certain
> exception. (I'm fuzzy on the details but I can help you with this when
> the time comes.) We'd write a new method just for use in the web
> service, which wrapped an updateLifecycleStatus() call with
> appropriate exception-raising code.
>
> (At the very least, call this operation something like 'complete', and
> pass in the REQUEST_USER automatically--I don't think it makes much
> sense to complete a blueprint on someone else's behalf.)

I completely missed that this was exposed and there are no tests for
it. I would lean towards deferring it all to a later branch.

> = Questions about the tests =
>
> 1. You changed the default priority for a specification created through
> the factory to LOW, and then changed some tests that were assuming a
> factory-created specification started out UNDEFINED. Is it easy to
> leave the earlier behavior alone? Or are specifications with UNDEFINED
> priority considered invalid, or don't show up in lists, or anything else?

I can ...

Read more...

Revision history for this message
Leonard Richardson (leonardr) wrote :
Download full text (9.0 KiB)

> > I don't have any opinion on the whiteboard thing, but I'll make sure someone
> with an opinion looks at this branch.

Curtis, do you have any objection to publishing the blueprint whiteboard through the web service?

> That was something I picked up from the work I merged from ajmitch. goal
> is a bit more work as it is not a simple attribute, so I didn't bother
> working on that, I just left goalstatus exported for now.

I'd rather remove goalstatus for now (to avoid cluttering up the interface), or do a little work to export goal as well. I think exporting goal is just a matter of changing the Attribute to an IReference(schema=???), but I don't know what that ??? is. Is it a distro series? Project series? Some abstract class representing the union of all possible series? Curtis, do you know?

If we can figure this out, and the schema is something that's already exported through the web service, then it should be easy to export goal.

> > 2. "I added two new methods to IHasSpecifications, as if we exposed
> > the existing attributes we would serve the collections when just
> > getting the specification." Can you explain? There are many places
> > where we publish a collection associated with an entry. (Just within
> > IPerson we have 'languages', 'gpg_keys', 'pending_gpg_keys',
> > 'wiki_names', 'irc_nicknames', etc.) Just getting the person doesn't
> > give you these collections--it gives you links to the collections.
>
> Ok, I still think methods are more appropriate for this given parallels
> with e.g. searchTasks, but I can change if you feel strongly.

searchTasks() needs to be a named operation because it takes arguments. These attributes here are more like IBug.bug_tasks.

> Also, doesn't exposing collection attributes require them to have a
> traversal? I don't think these do, as there is just /~person/+specs that
> takes query arguments.

Exposing a _top-level_ collection requires a traversal, but lazr.restful automatically takes care of traversal from an object to one of its associated collections.

> > 3. Rather than publishing updateLifecycleStatus as a named operation, I
> > think you could publish it as the mutator for the 'completer' field. I
> > know that you're not publishing 'completer' right now, but I imagine
> > it will be published eventually, and publishing it now with a mutator
> > means that end-users will have to learn one less strangely-named named
> > operation.
> >
> > The only catch is that updateLifecycleStatus() returns None if the
> > lifecycle is not in a state for being completed. It would be better to
> > return a 400 response code, which probably means raising a certain
> > exception. (I'm fuzzy on the details but I can help you with this when
> > the time comes.) We'd write a new method just for use in the web
> > service, which wrapped an updateLifecycleStatus() call with
> > appropriate exception-raising code.
> >
> > (At the very least, call this operation something like 'complete', and
> > pass in the REQUEST_USER automatically--I don't think it makes much
> > sense to complete a blueprint on someone else's behalf.)
>
> I completely missed that this was exposed and there are no tests for
> it....

Read more...

Revision history for this message
James Westby (james-w) wrote :

> I'd rather remove goalstatus for now (to avoid cluttering up the interface),
> or do a little work to export goal as well. I think exporting goal is just a
> matter of changing the Attribute to an IReference(schema=???), but I don't
> know what that ??? is. Is it a distro series? Project series? Some abstract
> class representing the union of all possible series? Curtis, do you know?

It's a distroseries or productseries, or possibly other things too, I'm not sure.
I doubt there is a single schema for it.

> If we can figure this out, and the schema is something that's already exported
> through the web service, then it should be easy to export goal.

I'll remove goalstatus from this branch.

> Exposing a _top-level_ collection requires a traversal, but lazr.restful
> automatically takes care of traversal from an object to one of its associated
> collections.

Ok, great.

> Yes, I missed getSpecification when I wrote this.
>
> Try something like this:
>
> def getSpecOnWebservice(self, spec_object):
> ...
> pillar = launchpadlib.load(str(launchpadlib._root_uri) + '/' +
> pillar_name)
> return pillar.getSpecification(spec_object.name)

Ok.

> Oh, another thing I forgot to mention is that once we/you upgrade Launchpad's
> launchpadlib, you should be able to pass relative URLs into Launchpad.load()

Yes, that's why I started down this path, but I can't land it as a prerequisite
branch yet.

> Fair enough. Would you be okay with a test that compared the entire
> representation as received by launchpadlib against some expected value? Take a
> look at lib/lp/bugs/stories/webservice/xx-bug.txt. It uses pprint_entry to
> display the full representation of an object derived from a JSON document.

What is the value in that test? That no attribute is exported without tests
being added?

> The difference between that test and your test is that your test does an end-
> to-end test with launchpadlib. From launchpadlib you can get an object that
> can be passed into pprint_entry with:
>
> entry._wadl_resource.representation
>
> I know that's a little ugly but if you're interested in using this in tests,
> we can make it part of the official launchpadlib API.

Well, I'm happy making a direct webservice call and comparing the parsed
JSON, I'm just not sure of the value.

> I believe so. Call it WebServiceClientLayer? There was a time when a
> launchpadlib instance was started up in one of the existing layers, but we
> removed that because it slowed the tests down too much.

The problem with a layer is what credentials does it get? Are we going to
end up with a really expensive layer that creates 5 different launchpadlib
objects with different credentials, or 5 different layers?

> As a quick hack, defining it in the setup() method would at least let you
> share it within a test class.

No it wouldn't, setUp is called once per test, unless I misunderstand what
you mean.

Thanks,

James

Revision history for this message
Curtis Hovey (sinzui) wrote :

1. It is fine to export whiteboards because they are used. We vowed to stop adding them because they are a poor substitute for a description/wiki.

2. series goal is...well yuck. I do not have a lot of confidence that this implementation is safe. It must be impossible fo a user to directly change the productseries and distroseries--it is impossible for both to have a series. .proposeGoal() has to be exposed, and .goal is used to retrieve it. I think distroseries and productseries should not be exported. ISeries is the generic type for .goal, there are productseries and distroseries are the two implementers

2.5 likewise, I do not think product and distribution should be exported, target returns the correct value and retarget() allows you to set it. target is ISpecificationTarget

Blueprints uses the view to validate when the model or an interface invarant should be doing the validation. Since I do not see any view changes I think we need a branch to land prior to this one that makes the model safe to export.
* SpecificationRetargetingView knows that names can colide, but the model does not.
* updateLifecycleStatus() is called often when editing a spec, but will users know to do this? I think the design is flawed.

review: Needs Fixing (curtis)
Revision history for this message
Robert Collins (lifeless) wrote :

Setting to WIP to make it clear its been reviewed - please put back to needs-review when curtis' comments are addressed ;)

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

https://code.edge.launchpad.net/~salgado/launchpad/safe-blueprints-model/+merge/41722 addresses some of Curtis' comments, but I only saw them after proposing my branch for merging, so I may have missed some things.

I've also filed a bug for getting rid of the sole remaining call to updateLifecycleStatus() in browser code (bug 680880). I should also note that updateLifecycleStatus() is only used once there and once in lp/blueprints/subscribers.py -- all other uses of it are in test code.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/canonical/launchpad/doc/tales.txt'
2--- lib/canonical/launchpad/doc/tales.txt 2010-05-21 14:58:50 +0000
3+++ lib/canonical/launchpad/doc/tales.txt 2010-07-15 16:19:46 +0000
4@@ -669,8 +669,11 @@
5 Blueprints
6 ..........
7
8+ >>> from lp.blueprints.interfaces.specification import (
9+ ... SpecificationPriority)
10 >>> login('test@canonical.com')
11- >>> specification = factory.makeSpecification()
12+ >>> specification = factory.makeSpecification(
13+ ... priority=SpecificationPriority.UNDEFINED)
14 >>> test_tales("specification/fmt:link", specification=specification)
15 u'<a...class="sprite blueprint-undefined">...</a>'
16
17@@ -678,7 +681,8 @@
18 Blueprint branches
19 ..................
20
21- >>> specification = factory.makeSpecification()
22+ >>> specification = factory.makeSpecification(
23+ ... priority=SpecificationPriority.UNDEFINED)
24 >>> branch = factory.makeAnyBranch()
25 >>> specification_branch = specification.linkBranch(branch, branch.owner)
26 >>> test_tales("specification_branch/fmt:link",
27
28=== modified file 'lib/canonical/launchpad/interfaces/_schema_circular_imports.py'
29--- lib/canonical/launchpad/interfaces/_schema_circular_imports.py 2010-07-13 15:29:08 +0000
30+++ lib/canonical/launchpad/interfaces/_schema_circular_imports.py 2010-07-15 16:19:46 +0000
31@@ -43,6 +43,8 @@
32 from lp.blueprints.interfaces.specification import ISpecification
33 from lp.blueprints.interfaces.specificationbranch import (
34 ISpecificationBranch)
35+from lp.blueprints.interfaces.specificationtarget import (
36+ IHasSpecifications, ISpecificationTarget)
37 from lp.code.interfaces.branch import IBranch
38 from lp.code.interfaces.branchmergeproposal import IBranchMergeProposal
39 from lp.code.interfaces.branchsubscription import IBranchSubscription
40@@ -433,3 +435,13 @@
41
42 # IProductSeries
43 patch_reference_property(IProductSeries, 'product', IProduct)
44+
45+# IHasSpecifications
46+patch_collection_return_type(
47+ IHasSpecifications, 'getAllSpecifications', ISpecification)
48+patch_collection_return_type(
49+ IHasSpecifications, 'getValidSpecifications', ISpecification)
50+
51+# ISpecificationTarget
52+patch_entry_return_type(
53+ ISpecificationTarget, 'getSpecification', ISpecification)
54
55=== modified file 'lib/lp/blueprints/interfaces/specification.py'
56--- lib/lp/blueprints/interfaces/specification.py 2010-02-19 12:05:10 +0000
57+++ lib/lp/blueprints/interfaces/specification.py 2010-07-15 16:19:46 +0000
58@@ -22,32 +22,36 @@
59 'SpecificationImplementationStatus',
60 'SpecificationLifecycleStatus',
61 'SpecificationPriority',
62- 'SpecificationSort'
63+ 'SpecificationSort',
64 ]
65
66
67 from lazr.restful.declarations import (
68- REQUEST_USER, call_with, export_as_webservice_entry,
69- export_write_operation, operation_parameters, operation_returns_entry)
70-from lazr.restful.fields import Reference
71+ exported, export_as_webservice_entry)
72+from lazr.restful.fields import ReferenceChoice
73 from zope.interface import Interface, Attribute
74 from zope.component import getUtility
75
76 from zope.schema import Datetime, Int, Choice, Text, TextLine, Bool
77
78 from canonical.launchpad import _
79+from canonical.launchpad.interfaces.validation import valid_webref
80 from canonical.launchpad.fields import (
81 ContentNameField, PublicPersonChoice, Summary, Title)
82 from canonical.launchpad.validators import LaunchpadValidationError
83-from lp.registry.interfaces.role import IHasOwner
84-from lp.code.interfaces.branch import IBranch
85+from lp.blueprints.interfaces.sprint import ISprint
86+from lp.blueprints.interfaces.specificationtarget import (
87+ IHasSpecifications)
88 from lp.code.interfaces.branchlink import IHasLinkedBranches
89+from lp.registry.interfaces.distribution import IDistribution
90+from lp.registry.interfaces.distroseries import IDistroSeries
91 from lp.registry.interfaces.mentoringoffer import ICanBeMentored
92-from canonical.launchpad.interfaces.validation import valid_webref
93+from lp.registry.interfaces.milestone import IMilestone
94 from lp.registry.interfaces.projectgroup import IProjectGroup
95-from lp.blueprints.interfaces.sprint import ISprint
96-from lp.blueprints.interfaces.specificationtarget import (
97- IHasSpecifications)
98+from lp.registry.interfaces.product import IProduct
99+from lp.registry.interfaces.productseries import IProductSeries
100+from lp.registry.interfaces.role import IHasOwner
101+
102
103 from lazr.enum import (
104 DBEnumeratedType, DBItem, EnumeratedType, Item)
105@@ -125,7 +129,8 @@
106 GOOD = DBItem(70, """
107 Good progress
108
109- The feature is considered on track for delivery in the targeted release.
110+ The feature is considered on track for delivery in the targeted
111+ release.
112 """)
113
114 BETA = DBItem(75, """
115@@ -148,8 +153,8 @@
116 AWAITINGDEPLOYMENT = DBItem(85, """
117 Deployment
118
119- The implementation has been done, and can be deployed in the production
120- environment, but this has not yet been done by the system
121+ The implementation has been done, and can be deployed in the
122+ production environment, but this has not yet been done by the system
123 administrators. (This status is typically used for Web services where
124 code is not released but instead is pushed into production.
125 """)
126@@ -441,8 +446,8 @@
127 NEW = DBItem(40, """
128 New
129
130- No thought has yet been given to implementation strategy, dependencies,
131- or presentation/UI issues.
132+ No thought has yet been given to implementation strategy,
133+ dependencies, or presentation/UI issues.
134 """)
135
136 SUPERSEDED = DBItem(60, """
137@@ -554,49 +559,58 @@
138 class INewSpecification(Interface):
139 """A schema for a new specification."""
140
141- name = SpecNameField(
142- title=_('Name'), required=True, readonly=False,
143- description=_(
144- "May contain lower-case letters, numbers, and dashes. "
145- "It will be used in the specification url. "
146- "Examples: mozilla-type-ahead-find, postgres-smart-serial.")
147- )
148- title = Title(
149- title=_('Title'), required=True, description=_(
150- "Describe the feature as clearly as possible in up to 70 "
151- "characters. This title is displayed in every feature "
152- "list or report."))
153- specurl = SpecURLField(
154- title=_('Specification URL'), required=False,
155- description=_(
156- "The URL of the specification. This is usually a wiki page."),
157- constraint=valid_webref)
158- summary = Summary(
159- title=_('Summary'), required=True, description=_(
160- "A single-paragraph description of the feature. "
161- "This will also be displayed in most feature listings."))
162- definition_status = Choice(
163- title=_('Definition Status'),
164- vocabulary=SpecificationDefinitionStatus,
165- default=SpecificationDefinitionStatus.NEW,
166- description=_(
167- "The current status of the process to define the "
168- "feature and get approval for the implementation plan."))
169- assignee = PublicPersonChoice(
170- title=_('Assignee'), required=False,
171- description=_("The person responsible for implementing the feature."),
172- vocabulary='ValidPersonOrTeam')
173- drafter = PublicPersonChoice(
174- title=_('Drafter'), required=False,
175- description=_(
176- "The person responsible for drafting the specification."),
177- vocabulary='ValidPersonOrTeam')
178- approver = PublicPersonChoice(
179- title=_('Approver'), required=False,
180- description=_(
181- "The person responsible for approving the specification, "
182- "and for reviewing the code when it's ready to be landed."),
183- vocabulary='ValidPersonOrTeam')
184+ name = exported(
185+ SpecNameField(
186+ title=_('Name'), required=True, readonly=False,
187+ description=_(
188+ "May contain lower-case letters, numbers, and dashes. "
189+ "It will be used in the specification url. "
190+ "Examples: mozilla-type-ahead-find, postgres-smart-serial.")))
191+ title = exported(
192+ Title(
193+ title=_('Title'), required=True, description=_(
194+ "Describe the feature as clearly as possible in up to 70 "
195+ "characters. This title is displayed in every feature "
196+ "list or report.")))
197+ specurl = exported(
198+ SpecURLField(
199+ title=_('Specification URL'), required=False,
200+ description=_(
201+ "The URL of the specification. This is usually a wiki page."),
202+ constraint=valid_webref),
203+ exported_as="specification_url")
204+ summary = exported(
205+ Summary(
206+ title=_('Summary'), required=True, description=_(
207+ "A single-paragraph description of the feature. "
208+ "This will also be displayed in most feature listings.")))
209+ definition_status = exported(
210+ Choice(
211+ title=_('Definition Status'),
212+ vocabulary=SpecificationDefinitionStatus,
213+ default=SpecificationDefinitionStatus.NEW,
214+ description=_(
215+ "The current status of the process to define the "
216+ "feature and get approval for the implementation plan.")))
217+ assignee = exported(
218+ PublicPersonChoice(
219+ title=_('Assignee'), required=False,
220+ description=_(
221+ "The person responsible for implementing the feature."),
222+ vocabulary='ValidPersonOrTeam'))
223+ drafter = exported(
224+ PublicPersonChoice(
225+ title=_('Drafter'), required=False,
226+ description=_(
227+ "The person responsible for drafting the specification."),
228+ vocabulary='ValidPersonOrTeam'))
229+ approver = exported(
230+ PublicPersonChoice(
231+ title=_('Approver'), required=False,
232+ description=_(
233+ "The person responsible for approving the specification, "
234+ "and for reviewing the code when it's ready to be landed."),
235+ vocabulary='ValidPersonOrTeam'))
236
237
238 class INewSpecificationProjectTarget(Interface):
239@@ -645,7 +659,10 @@
240
241 class ISpecification(INewSpecification, INewSpecificationTarget, IHasOwner,
242 ICanBeMentored, IHasLinkedBranches):
243- """A Specification."""
244+ """A Specification.
245+
246+ Also known as a blueprint.
247+ """
248
249 export_as_webservice_entry()
250
251@@ -655,46 +672,62 @@
252 # referencing it.
253 id = Int(title=_("Database ID"), required=True, readonly=True)
254
255- priority = Choice(
256- title=_('Priority'), vocabulary=SpecificationPriority,
257- default=SpecificationPriority.UNDEFINED, required=True)
258- datecreated = Datetime(
259- title=_('Date Created'), required=True, readonly=True)
260- owner = PublicPersonChoice(
261- title=_('Owner'), required=True, readonly=True,
262- vocabulary='ValidPersonOrTeam')
263+ priority = exported(
264+ Choice(
265+ title=_('Priority'), vocabulary=SpecificationPriority,
266+ default=SpecificationPriority.UNDEFINED, required=True))
267+ datecreated = exported(
268+ Datetime(
269+ title=_('Date Created'), required=True, readonly=True),
270+ exported_as='date_created')
271+ owner = exported(
272+ PublicPersonChoice(
273+ title=_('Owner'), required=True, readonly=True,
274+ vocabulary='ValidPersonOrTeam'))
275 # target
276- product = Choice(title=_('Project'), required=False,
277- vocabulary='Product')
278- distribution = Choice(title=_('Distribution'), required=False,
279- vocabulary='Distribution')
280+ product = exported(
281+ ReferenceChoice(title=_('Project'), required=False,
282+ vocabulary='Product', schema=IProduct),
283+ exported_as='project')
284+ distribution = exported(
285+ ReferenceChoice(title=_('Distribution'), required=False,
286+ vocabulary='Distribution', schema=IDistribution))
287
288 # series
289- productseries = Choice(title=_('Series Goal'), required=False,
290- vocabulary='FilteredProductSeries',
291- description=_(
292- "Choose a series in which you would like to deliver "
293- "this feature. Selecting '(no value)' will clear the goal."))
294- distroseries = Choice(title=_('Series Goal'), required=False,
295- vocabulary='FilteredDistroSeries',
296- description=_(
297- "Choose a series in which you would like to deliver "
298- "this feature. Selecting '(no value)' will clear the goal."))
299+ productseries = exported(
300+ ReferenceChoice(title=_('Series Goal'), required=False,
301+ vocabulary='FilteredProductSeries',
302+ description=_(
303+ "Choose a series in which you would like to deliver "
304+ "this feature. Selecting '(no value)' will clear the goal."),
305+ schema=IProductSeries),
306+ exported_as='project_series')
307+ distroseries = exported(
308+ ReferenceChoice(title=_('Series Goal'), required=False,
309+ vocabulary='FilteredDistroSeries',
310+ description=_(
311+ "Choose a series in which you would like to deliver "
312+ "this feature. Selecting '(no value)' will clear the goal."),
313+ schema=IDistroSeries))
314
315 # milestone
316- milestone = Choice(
317- title=_('Milestone'), required=False, vocabulary='Milestone',
318- description=_(
319- "The milestone in which we would like this feature to be "
320- "delivered."))
321+ milestone = exported(
322+ ReferenceChoice(
323+ title=_('Milestone'), required=False, vocabulary='Milestone',
324+ description=_(
325+ "The milestone in which we would like this feature to be "
326+ "delivered."),
327+ schema=IMilestone))
328
329 # nomination to a series for release management
330 goal = Attribute("The series for which this feature is a goal.")
331- goalstatus = Choice(
332- title=_('Goal Acceptance'), vocabulary=SpecificationGoalStatus,
333- default=SpecificationGoalStatus.PROPOSED, description=_(
334- "Whether or not the drivers have accepted this feature as "
335- "a goal for the targeted series."))
336+ goalstatus = exported(
337+ Choice(
338+ title=_('Goal Acceptance'), vocabulary=SpecificationGoalStatus,
339+ default=SpecificationGoalStatus.PROPOSED, description=_(
340+ "Whether or not the drivers have accepted this feature as "
341+ "a goal for the targeted series.")),
342+ exported_as='goal_status')
343 goal_proposer = Attribute("The person who nominated the spec for "
344 "this series.")
345 date_goal_proposed = Attribute("The date of the nomination.")
346@@ -703,10 +736,11 @@
347 date_goal_decided = Attribute("The date the spec was approved "
348 "or declined as a goal.")
349
350- whiteboard = Text(title=_('Status Whiteboard'), required=False,
351- description=_(
352- "Any notes on the status of this spec you would like to make. "
353- "Your changes will override the current text."))
354+ whiteboard = exported(
355+ Text(title=_('Status Whiteboard'), required=False,
356+ description=_(
357+ "Any notes on the status of this spec you would like to "
358+ "make. Your changes will override the current text.")))
359 direction_approved = Bool(title=_('Basic direction approved?'),
360 required=False, default=False, description=_("Check this to "
361 "indicate that the drafter and assignee have satisfied the "
362@@ -880,7 +914,6 @@
363 """Return the SpecificationBranch link for the branch, or None."""
364
365
366-# Interfaces for containers
367 class ISpecificationSet(IHasSpecifications):
368 """A container for specifications."""
369
370
371=== modified file 'lib/lp/blueprints/interfaces/specificationtarget.py'
372--- lib/lp/blueprints/interfaces/specificationtarget.py 2009-08-20 14:00:52 +0000
373+++ lib/lp/blueprints/interfaces/specificationtarget.py 2010-07-15 16:19:46 +0000
374@@ -14,6 +14,12 @@
375 ]
376
377 from zope.interface import Interface, Attribute
378+from zope.schema import TextLine
379+
380+from canonical.launchpad import _
381+from lazr.restful.declarations import (
382+ export_read_operation, operation_parameters,
383+ operation_returns_collection_of, operation_returns_entry)
384
385
386 class IHasSpecifications(Interface):
387@@ -59,6 +65,15 @@
388 situations in which these are not rendered.
389 """
390
391+ @operation_returns_collection_of(Interface) # really ISpecification
392+ @export_read_operation()
393+ def getAllSpecifications():
394+ """Return all the specifications associated with this object."""
395+
396+ @operation_returns_collection_of(Interface) # really ISpecification
397+ @export_read_operation()
398+ def getValidSpecifications():
399+ """Return all the non-obsolete specifications for this object."""
400
401
402 class ISpecificationTarget(IHasSpecifications):
403@@ -66,6 +81,10 @@
404 specifications directly attached to them.
405 """
406
407+ @operation_parameters(
408+ name=TextLine(title=_('The name of the specification')))
409+ @operation_returns_entry(Interface) # really ISpecification
410+ @export_read_operation()
411 def getSpecification(name):
412 """Returns the specification with the given name, for this target,
413 or None.
414
415=== modified file 'lib/lp/blueprints/model/specification.py'
416--- lib/lp/blueprints/model/specification.py 2009-09-21 14:56:07 +0000
417+++ lib/lp/blueprints/model/specification.py 2010-07-15 16:19:46 +0000
418@@ -229,7 +229,7 @@
419 # and make sure there is no leftover distroseries goal
420 self.productseries = None
421 else:
422- raise AssertionError, 'Inappropriate goal.'
423+ raise AssertionError('Inappropriate goal.')
424 # record who made the proposal, and when
425 self.goal_proposer = proposer
426 self.date_goal_proposed = UTC_NOW
427@@ -685,6 +685,14 @@
428 """See IHasSpecifications."""
429 return self.specifications(filter=[SpecificationFilter.ALL]).count()
430
431+ def getAllSpecifications(self):
432+ """See IHasSpecifications."""
433+ return self.all_specifications
434+
435+ def getValidSpecifications(self):
436+ """See IHasSpecifications."""
437+ return self.valid_specifications
438+
439
440 class SpecificationSet(HasSpecificationsMixin):
441 """The set of feature specifications."""
442@@ -790,7 +798,7 @@
443
444 # filter based on completion. see the implementation of
445 # Specification.is_complete() for more details
446- completeness = Specification.completeness_clause
447+ completeness = Specification.completeness_clause
448
449 if SpecificationFilter.COMPLETE in filter:
450 query += ' AND ( %s ) ' % completeness
451@@ -838,13 +846,23 @@
452 def new(self, name, title, specurl, summary, definition_status,
453 owner, approver=None, product=None, distribution=None, assignee=None,
454 drafter=None, whiteboard=None,
455- priority=SpecificationPriority.UNDEFINED):
456+ priority=SpecificationPriority.UNDEFINED,
457+ goalstatus=SpecificationGoalStatus.PROPOSED,
458+ productseries=None, distroseries=None,
459+ goal_proposer=None, date_goal_proposed=None, milestone=None,
460+ date_completed=None, completer=None, goal_decider=None,
461+ date_goal_decided=None):
462 """See ISpecificationSet."""
463 return Specification(name=name, title=title, specurl=specurl,
464 summary=summary, priority=priority,
465 definition_status=definition_status, owner=owner,
466 approver=approver, product=product, distribution=distribution,
467- assignee=assignee, drafter=drafter, whiteboard=whiteboard)
468+ assignee=assignee, drafter=drafter, whiteboard=whiteboard,
469+ goalstatus=goalstatus, productseries=productseries,
470+ distroseries=distroseries, goal_proposer=goal_proposer,
471+ date_goal_proposed=date_goal_proposed, milestone=milestone,
472+ date_completed=date_completed, completer=completer,
473+ goal_decider=goal_decider, date_goal_decided=date_goal_decided)
474
475 def getDependencyDict(self, specifications):
476 """See `ISpecificationSet`."""
477@@ -859,7 +877,8 @@
478 FROM SpecificationDependency, Specification
479 WHERE SpecificationDependency.specification IN %s
480 AND SpecificationDependency.dependency = Specification.id
481- ORDER BY Specification.priority DESC, Specification.name, Specification.id
482+ ORDER BY Specification.priority DESC, Specification.name,
483+ Specification.id
484 """ % sqlvalues(specification_ids)).get_all()
485
486 dependencies = {}
487
488=== modified file 'lib/lp/blueprints/stories/standalone/sprint-links.txt'
489--- lib/lp/blueprints/stories/standalone/sprint-links.txt 2009-09-22 10:48:09 +0000
490+++ lib/lp/blueprints/stories/standalone/sprint-links.txt 2010-07-15 16:19:46 +0000
491@@ -13,10 +13,6 @@
492 >>> browser.open('http://blueprints.launchpad.dev/firefox/+spec/canvas')
493 >>> browser.isHtml
494 True
495- >>> 'Accepted' in browser.contents # make sure the page is not polluted
496- False
497- >>> 'Proposed' in browser.contents # make sure the page is not polluted
498- False
499
500 Then we are going to propose it for the meeting agenda:
501
502
503=== added file 'lib/lp/blueprints/tests/test_implements.py'
504--- lib/lp/blueprints/tests/test_implements.py 1970-01-01 00:00:00 +0000
505+++ lib/lp/blueprints/tests/test_implements.py 2010-07-15 16:19:46 +0000
506@@ -0,0 +1,61 @@
507+# Copyright 2009 Canonical Ltd. This software is licensed under the
508+# GNU Affero General Public License version 3 (see the file LICENSE).
509+
510+"""Tests that various objects implement specification-related interfaces."""
511+
512+__metaclass__ = type
513+
514+from canonical.testing import DatabaseFunctionalLayer
515+from lp.blueprints.interfaces.specificationtarget import (
516+ IHasSpecifications, ISpecificationTarget)
517+from lp.testing import TestCaseWithFactory
518+
519+
520+class ImplementsIHasSpecificationsTests(TestCaseWithFactory):
521+ """Test that various objects implement IHasSpecifications."""
522+ layer = DatabaseFunctionalLayer
523+
524+ def test_product_implements_IHasSpecifications(self):
525+ product = self.factory.makeProduct()
526+ self.assertProvides(product, IHasSpecifications)
527+
528+ def test_distribution_implements_IHasSpecifications(self):
529+ product = self.factory.makeProduct()
530+ self.assertProvides(product, IHasSpecifications)
531+
532+ def test_projectgroup_implements_IHasSpecifications(self):
533+ projectgroup = self.factory.makeProject()
534+ self.assertProvides(projectgroup, IHasSpecifications)
535+
536+ def test_person_implements_IHasSpecifications(self):
537+ person = self.factory.makePerson()
538+ self.assertProvides(person, IHasSpecifications)
539+
540+ def test_productseries_implements_IHasSpecifications(self):
541+ productseries = self.factory.makeProductSeries()
542+ self.assertProvides(productseries, IHasSpecifications)
543+
544+ def test_distroseries_implements_IHasSpecifications(self):
545+ distroseries = self.factory.makeDistroSeries()
546+ self.assertProvides(distroseries, IHasSpecifications)
547+
548+
549+class ImplementsISpecificationTargetTests(TestCaseWithFactory):
550+ """Test that various objects implement ISpecificationTarget."""
551+ layer = DatabaseFunctionalLayer
552+
553+ def test_product_implements_ISpecificationTarget(self):
554+ product = self.factory.makeProduct()
555+ self.assertProvides(product, ISpecificationTarget)
556+
557+ def test_distribution_implements_ISpecificationTarget(self):
558+ product = self.factory.makeProduct()
559+ self.assertProvides(product, ISpecificationTarget)
560+
561+ def test_productseries_implements_ISpecificationTarget(self):
562+ productseries = self.factory.makeProductSeries()
563+ self.assertProvides(productseries, ISpecificationTarget)
564+
565+ def test_distroseries_implements_ISpecificationTarget(self):
566+ distroseries = self.factory.makeDistroSeries()
567+ self.assertProvides(distroseries, ISpecificationTarget)
568
569=== added file 'lib/lp/blueprints/tests/test_webservice.py'
570--- lib/lp/blueprints/tests/test_webservice.py 1970-01-01 00:00:00 +0000
571+++ lib/lp/blueprints/tests/test_webservice.py 2010-07-15 16:19:46 +0000
572@@ -0,0 +1,550 @@
573+# Copyright 2009 Canonical Ltd. This software is licensed under the
574+# GNU Affero General Public License version 3 (see the file LICENSE).
575+
576+"""Webservice unit tests related to Launchpad blueprints."""
577+
578+__metaclass__ = type
579+
580+from canonical.testing import DatabaseFunctionalLayer
581+from canonical.launchpad.testing.pages import webservice_for_person
582+from lp.blueprints.interfaces.specification import (
583+ SpecificationDefinitionStatus, SpecificationGoalStatus,
584+ SpecificationPriority)
585+from lp.testing import (
586+ launchpadlib_for, TestCaseWithFactory)
587+
588+
589+class SpecificationWebserviceTestCase(TestCaseWithFactory):
590+
591+ def makeProduct(self):
592+ return self.factory.makeProduct(name="fooix")
593+
594+ def makeDistribution(self):
595+ return self.factory.makeDistribution(name="foobuntu")
596+
597+ def getLaunchpadlib(self):
598+ user = self.factory.makePerson()
599+ return launchpadlib_for("testing", user)
600+
601+ def getSpecOnWebservice(self, spec_object):
602+ launchpadlib = self.getLaunchpadlib()
603+ if spec_object.product is not None:
604+ pillar_name = spec_object.product.name
605+ else:
606+ pillar_name = spec_object.distribution.name
607+ return launchpadlib.load(
608+ str(launchpadlib._root_uri) + '/%s/+spec/%s'
609+ % (pillar_name, spec_object.name))
610+
611+ def getPillarOnWebservice(self, pillar_obj):
612+ launchpadlib = self.getLaunchpadlib()
613+ return launchpadlib.load(
614+ str(launchpadlib._root_uri) + '/' + pillar_obj.name)
615+
616+
617+class SpecificationAttributeWebserviceTests(SpecificationWebserviceTestCase):
618+ """Test accessing specification attributes over the webservice."""
619+ layer = DatabaseFunctionalLayer
620+
621+ def makeSimpleSpecification(self):
622+ self.name = "some-spec"
623+ self.title = "some-title"
624+ self.url = "http://example.org/some_url"
625+ self.summary = "Some summary."
626+ definition_status = SpecificationDefinitionStatus.PENDINGAPPROVAL
627+ self.definition_status = definition_status.title
628+ self.assignee_name = "james-w"
629+ assignee = self.factory.makePerson(name=self.assignee_name)
630+ self.drafter_name = "jml"
631+ drafter = self.factory.makePerson(name=self.drafter_name)
632+ self.approver_name = "bob"
633+ approver = self.factory.makePerson(name=self.approver_name)
634+ self.owner_name = "mary"
635+ owner = self.factory.makePerson(name=self.owner_name)
636+ priority = SpecificationPriority.HIGH
637+ self.priority = priority.title
638+ goal_status = SpecificationGoalStatus.PROPOSED
639+ self.goal_status = goal_status.title
640+ self.whiteboard = "Some whiteboard"
641+ product = self.factory.makeProduct()
642+ return self.factory.makeSpecification(
643+ product=product, name=self.name,
644+ title=self.title, specurl=self.url,
645+ summary=self.summary,
646+ definition_status=definition_status,
647+ assignee=assignee, drafter=drafter, approver=approver,
648+ priority=priority,
649+ owner=owner, whiteboard=self.whiteboard, goalstatus=goal_status)
650+
651+ def getSimpleSpecificationResponse(self):
652+ self.spec_object = self.makeSimpleSpecification()
653+ return self.getSpecOnWebservice(self.spec_object)
654+
655+ def test_can_retrieve_representation(self):
656+ spec = self.makeSimpleSpecification()
657+ user = self.factory.makePerson()
658+ webservice = webservice_for_person(user)
659+ response = webservice.get(
660+ '/%s/+spec/%s' % (spec.product.name, spec.name))
661+ self.assertEqual(response.status, 200)
662+
663+ def test_representation_contains_name(self):
664+ spec = self.getSimpleSpecificationResponse()
665+ self.assertEqual(self.name, spec.name)
666+
667+ def test_representation_contains_title(self):
668+ spec = self.getSimpleSpecificationResponse()
669+ self.assertEqual(self.title, spec.title)
670+
671+ def test_representation_contains_specification_url(self):
672+ spec = self.getSimpleSpecificationResponse()
673+ self.assertEqual(self.url, spec.specification_url)
674+
675+ def test_representation_contains_summary(self):
676+ spec = self.getSimpleSpecificationResponse()
677+ self.assertEqual(self.summary, spec.summary)
678+
679+ def test_representation_contains_definition_status(self):
680+ spec = self.getSimpleSpecificationResponse()
681+ self.assertEqual(
682+ self.definition_status, spec.definition_status)
683+
684+ def test_representation_contains_assignee(self):
685+ spec = self.getSimpleSpecificationResponse()
686+ self.assertEqual(self.assignee_name, spec.assignee.name)
687+
688+ def test_representation_contains_drafter(self):
689+ spec = self.getSimpleSpecificationResponse()
690+ self.assertEqual(self.drafter_name, spec.drafter.name)
691+
692+ def test_representation_contains_approver(self):
693+ spec = self.getSimpleSpecificationResponse()
694+ self.assertEqual(self.approver_name, spec.approver.name)
695+
696+ def test_representation_contains_owner(self):
697+ spec = self.getSimpleSpecificationResponse()
698+ self.assertEqual(self.owner_name, spec.owner.name)
699+
700+ def test_representation_contains_priority(self):
701+ spec = self.getSimpleSpecificationResponse()
702+ self.assertEqual(self.priority, spec.priority)
703+
704+ def test_representation_contains_date_created(self):
705+ spec = self.getSimpleSpecificationResponse()
706+ self.assertEqual(self.spec_object.datecreated, spec.date_created)
707+
708+ def test_representation_contains_goal_status(self):
709+ spec = self.getSimpleSpecificationResponse()
710+ self.assertEqual(self.goal_status, spec.goal_status)
711+
712+ def test_representation_contains_whiteboard(self):
713+ spec = self.getSimpleSpecificationResponse()
714+ self.assertEqual(self.whiteboard, spec.whiteboard)
715+
716+ def test_representation_with_no_whiteboard(self):
717+ product = self.makeProduct()
718+ name = "some-spec"
719+ spec_object = self.factory.makeSpecification(
720+ product=product, name=name, whiteboard=None)
721+ # Check that the factory didn't add a whiteboard
722+ self.assertEqual(None, spec_object.whiteboard)
723+ spec = self.getSpecOnWebservice(spec_object)
724+ # Check that it is None on the webservice too
725+ self.assertEqual(None, spec.whiteboard)
726+
727+ def test_representation_with_no_approver(self):
728+ product = self.makeProduct()
729+ name = "some-spec"
730+ spec_object = self.factory.makeSpecification(
731+ product=product, name=name, approver=None)
732+ # Check that the factory didn't add an approver
733+ self.assertEqual(None, spec_object.approver)
734+ spec = self.getSpecOnWebservice(spec_object)
735+ # Check that it is None on the webservice too
736+ self.assertEqual(None, spec.approver)
737+
738+ def test_representation_with_no_drafter(self):
739+ product = self.makeProduct()
740+ name = "some-spec"
741+ spec_object = self.factory.makeSpecification(
742+ product=product, name=name, drafter=None)
743+ # Check that the factory didn't add an drafter
744+ self.assertEqual(None, spec_object.drafter)
745+ spec = self.getSpecOnWebservice(spec_object)
746+ # Check that it is None on the webservice too
747+ self.assertEqual(None, spec.drafter)
748+
749+ def test_representation_with_no_assignee(self):
750+ product = self.makeProduct()
751+ name = "some-spec"
752+ spec_object = self.factory.makeSpecification(
753+ product=product, name=name, assignee=None)
754+ # Check that the factory didn't add an assignee
755+ self.assertEqual(None, spec_object.assignee)
756+ spec = self.getSpecOnWebservice(spec_object)
757+ # Check that it is None on the webservice too
758+ self.assertEqual(None, spec.assignee)
759+
760+ def test_representation_with_no_specification_url(self):
761+ product = self.makeProduct()
762+ name = "some-spec"
763+ spec_object = self.factory.makeSpecification(
764+ product=product, name=name, specurl=None)
765+ # Check that the factory didn't add an specurl
766+ self.assertEqual(None, spec_object.specurl)
767+ spec = self.getSpecOnWebservice(spec_object)
768+ # Check that it is None on the webservice too
769+ self.assertEqual(None, spec.specification_url)
770+
771+ def test_representation_has_project_link(self):
772+ product = self.makeProduct()
773+ name = "some-spec"
774+ spec_object = self.factory.makeSpecification(
775+ product=product, name=name)
776+ spec = self.getSpecOnWebservice(spec_object)
777+ self.assertEqual('fooix', spec.project.name)
778+
779+ def test_representation_has_project_series_link(self):
780+ product = self.makeProduct()
781+ productseries = self.factory.makeProductSeries(
782+ name='fooix-dev', product=product)
783+ name = "some-spec"
784+ spec_object = self.factory.makeSpecification(
785+ product=product, name=name, productseries=productseries)
786+ spec = self.getSpecOnWebservice(spec_object)
787+ self.assertEqual('fooix-dev', spec.project_series.name)
788+
789+ def test_representation_has_distribution_link(self):
790+ distribution = self.makeDistribution()
791+ name = "some-spec"
792+ spec_object = self.factory.makeSpecification(
793+ distribution=distribution, name=name)
794+ spec = self.getSpecOnWebservice(spec_object)
795+ self.assertEqual('foobuntu', spec.distribution.name)
796+
797+ def test_representation_has_distroseries_link(self):
798+ distribution = self.makeDistribution()
799+ distroseries = self.factory.makeDistroSeries(
800+ name='maudlin', distribution=distribution)
801+ name = "some-spec"
802+ spec_object = self.factory.makeSpecification(
803+ distribution=distribution, name=name, distroseries=distroseries)
804+ spec = self.getSpecOnWebservice(spec_object)
805+ self.assertEqual('maudlin', spec.distroseries.name)
806+
807+ def test_representation_empty_distribution(self):
808+ product = self.makeProduct()
809+ name = "some-spec"
810+ spec_object = self.factory.makeSpecification(
811+ product=product, name=name)
812+ # Check that we didn't pick one up in the factory
813+ self.assertEqual(None, spec_object.distribution)
814+ spec = self.getSpecOnWebservice(spec_object)
815+ self.assertEqual(None, spec.distribution)
816+
817+ def test_representation_empty_project_series(self):
818+ product = self.makeProduct()
819+ name = "some-spec"
820+ spec_object = self.factory.makeSpecification(
821+ product=product, name=name)
822+ # Check that we didn't pick one up in the factory
823+ self.assertEqual(None, spec_object.productseries)
824+ spec = self.getSpecOnWebservice(spec_object)
825+ self.assertEqual(None, spec.project_series)
826+
827+ def test_representation_empty_project(self):
828+ distribution = self.makeDistribution()
829+ name = "some-spec"
830+ spec_object = self.factory.makeSpecification(
831+ distribution=distribution, name=name)
832+ # Check that we didn't pick one up in the factory
833+ self.assertEqual(None, spec_object.product)
834+ spec = self.getSpecOnWebservice(spec_object)
835+ self.assertEqual(None, spec.project)
836+
837+ def test_representation_empty_distroseries(self):
838+ distribution = self.makeDistribution()
839+ name = "some-spec"
840+ spec_object = self.factory.makeSpecification(
841+ distribution=distribution, name=name)
842+ # Check that we didn't pick one up in the factory
843+ self.assertEqual(None, spec_object.distroseries)
844+ spec = self.getSpecOnWebservice(spec_object)
845+ self.assertEqual(None, spec.distroseries)
846+
847+ def test_representation_contains_milestone(self):
848+ product = self.makeProduct()
849+ productseries = self.factory.makeProductSeries(product=product)
850+ milestone = self.factory.makeMilestone(
851+ name="1.0", product=product, productseries=productseries)
852+ spec_object = self.factory.makeSpecification(
853+ product=product, productseries=productseries, milestone=milestone)
854+ spec = self.getSpecOnWebservice(spec_object)
855+ self.assertEqual("1.0", spec.milestone.name)
856+
857+ def test_representation_empty_milestone(self):
858+ product = self.makeProduct()
859+ spec_object = self.factory.makeSpecification(
860+ product=product, milestone=None)
861+ # Check that the factory didn't add a milestone
862+ self.assertEqual(None, spec_object.milestone)
863+ spec = self.getSpecOnWebservice(spec_object)
864+ self.assertEqual(None, spec.milestone)
865+
866+
867+class SpecificationTargetTests(SpecificationWebserviceTestCase):
868+ """Tests for accessing specifications via their targets."""
869+ layer = DatabaseFunctionalLayer
870+
871+ def test_get_specification_on_product(self):
872+ product = self.makeProduct()
873+ spec_object = self.factory.makeSpecification(
874+ product=product, name="some-spec")
875+ product_on_webservice = self.getPillarOnWebservice(product)
876+ spec = product_on_webservice.getSpecification(name="some-spec")
877+ self.assertEqual("some-spec", spec.name)
878+ self.assertEqual("fooix", spec.project.name)
879+
880+ def test_get_specification_not_found(self):
881+ product = self.makeProduct()
882+ product_on_webservice = self.getPillarOnWebservice(product)
883+ spec = product_on_webservice.getSpecification(name="nonexistant")
884+ self.assertEqual(None, spec)
885+
886+ def test_get_specification_on_distribution(self):
887+ distribution = self.makeDistribution()
888+ spec_object = self.factory.makeSpecification(
889+ distribution=distribution, name="some-spec")
890+ distro_on_webservice = self.getPillarOnWebservice(distribution)
891+ spec = distro_on_webservice.getSpecification(name="some-spec")
892+ self.assertEqual("some-spec", spec.name)
893+ self.assertEqual("foobuntu", spec.distribution.name)
894+
895+ def test_get_specification_on_productseries(self):
896+ product = self.makeProduct()
897+ productseries = self.factory.makeProductSeries(
898+ product=product, name="fooix-dev")
899+ spec_object = self.factory.makeSpecification(
900+ product=product, name="some-spec", productseries=productseries)
901+ product_on_webservice = self.getPillarOnWebservice(product)
902+ productseries_on_webservice = product_on_webservice.getSeries(
903+ name="fooix-dev")
904+ spec = productseries_on_webservice.getSpecification(name="some-spec")
905+ self.assertEqual("some-spec", spec.name)
906+ self.assertEqual("fooix", spec.project.name)
907+ self.assertEqual("fooix-dev", spec.project_series.name)
908+
909+ def test_get_specification_on_distroseries(self):
910+ distribution = self.makeDistribution()
911+ distroseries = self.factory.makeDistroSeries(
912+ distribution=distribution, name="maudlin")
913+ spec_object = self.factory.makeSpecification(
914+ distribution=distribution, name="some-spec",
915+ distroseries=distroseries)
916+ distro_on_webservice = self.getPillarOnWebservice(distribution)
917+ distroseries_on_webservice = distro_on_webservice.getSeries(
918+ name_or_version="maudlin")
919+ spec = distroseries_on_webservice.getSpecification(name="some-spec")
920+ self.assertEqual("some-spec", spec.name)
921+ self.assertEqual("foobuntu", spec.distribution.name)
922+ self.assertEqual("maudlin", spec.distroseries.name)
923+
924+
925+class IHasSpecificationsTests(SpecificationWebserviceTestCase):
926+ """Tests for accessing IHasSpecifications methods over the webservice."""
927+ layer = DatabaseFunctionalLayer
928+
929+ def assertNamesOfSpecificationsAre(self, names, specifications):
930+ self.assertEqual(names, [s.name for s in specifications])
931+
932+ def test_product_getAllSpecifications(self):
933+ product = self.makeProduct()
934+ self.factory.makeSpecification(product=product, name="spec1")
935+ self.factory.makeSpecification(product=product, name="spec2")
936+ product_on_webservice = self.getPillarOnWebservice(product)
937+ self.assertNamesOfSpecificationsAre(
938+ ["spec1", "spec2"], product_on_webservice.getAllSpecifications())
939+
940+ def test_product_getValidSpecifications(self):
941+ product = self.makeProduct()
942+ self.factory.makeSpecification(product=product, name="spec1")
943+ self.factory.makeSpecification(
944+ product=product, name="spec2",
945+ definition_status=SpecificationDefinitionStatus.OBSOLETE)
946+ product_on_webservice = self.getPillarOnWebservice(product)
947+ self.assertNamesOfSpecificationsAre(
948+ ["spec1"], product_on_webservice.getValidSpecifications())
949+
950+ def test_distribution_getAllSpecifications(self):
951+ distribution = self.makeDistribution()
952+ self.factory.makeSpecification(
953+ distribution=distribution, name="spec1")
954+ self.factory.makeSpecification(
955+ distribution=distribution, name="spec2")
956+ distro_on_webservice = self.getPillarOnWebservice(distribution)
957+ self.assertNamesOfSpecificationsAre(
958+ ["spec1", "spec2"], distro_on_webservice.getAllSpecifications())
959+
960+ def test_distribution_getValidSpecifications(self):
961+ distribution = self.makeDistribution()
962+ self.factory.makeSpecification(
963+ distribution=distribution, name="spec1")
964+ self.factory.makeSpecification(
965+ distribution=distribution, name="spec2",
966+ definition_status=SpecificationDefinitionStatus.OBSOLETE)
967+ distro_on_webservice = self.getPillarOnWebservice(distribution)
968+ self.assertNamesOfSpecificationsAre(
969+ ["spec1"], distro_on_webservice.getValidSpecifications())
970+
971+ def test_distroseries_getAllSpecifications(self):
972+ distribution = self.makeDistribution()
973+ distroseries = self.factory.makeDistroSeries(
974+ name='maudlin', distribution=distribution)
975+ self.factory.makeSpecification(
976+ distribution=distribution, name="spec1",
977+ distroseries=distroseries)
978+ self.factory.makeSpecification(
979+ distribution=distribution, name="spec2",
980+ distroseries=distroseries)
981+ self.factory.makeSpecification(
982+ distribution=distribution, name="spec3")
983+ distro_on_webservice = self.getPillarOnWebservice(distribution)
984+ distroseries_on_webservice = distro_on_webservice.getSeries(
985+ name_or_version="maudlin")
986+ self.assertNamesOfSpecificationsAre(
987+ ["spec1", "spec2"],
988+ distroseries_on_webservice.getAllSpecifications())
989+
990+ def test_distroseries_getValidSpecifications(self):
991+ distribution = self.makeDistribution()
992+ distroseries = self.factory.makeDistroSeries(
993+ name='maudlin', distribution=distribution)
994+ self.factory.makeSpecification(
995+ distribution=distribution, name="spec1",
996+ distroseries=distroseries,
997+ goalstatus=SpecificationGoalStatus.ACCEPTED)
998+ self.factory.makeSpecification(
999+ distribution=distribution, name="spec2",
1000+ goalstatus=SpecificationGoalStatus.DECLINED,
1001+ distroseries=distroseries)
1002+ self.factory.makeSpecification(
1003+ distribution=distribution, name="spec3",
1004+ distroseries=distroseries,
1005+ goalstatus=SpecificationGoalStatus.ACCEPTED,
1006+ definition_status=SpecificationDefinitionStatus.OBSOLETE)
1007+ self.factory.makeSpecification(
1008+ distribution=distribution, name="spec4")
1009+ distro_on_webservice = self.getPillarOnWebservice(distribution)
1010+ distroseries_on_webservice = distro_on_webservice.getSeries(
1011+ name_or_version="maudlin")
1012+ self.assertNamesOfSpecificationsAre(
1013+ ["spec1", "spec3"],
1014+ distroseries_on_webservice.getValidSpecifications())
1015+
1016+ def test_productseries_getAllSpecifications(self):
1017+ product = self.makeProduct()
1018+ productseries = self.factory.makeProductSeries(
1019+ product=product, name="fooix-dev")
1020+ self.factory.makeSpecification(
1021+ product=product, name="spec1", productseries=productseries)
1022+ self.factory.makeSpecification(
1023+ product=product, name="spec2", productseries=productseries)
1024+ self.factory.makeSpecification(product=product, name="spec3")
1025+ product_on_webservice = self.getPillarOnWebservice(product)
1026+ series_on_webservice = product_on_webservice.getSeries(
1027+ name="fooix-dev")
1028+ self.assertNamesOfSpecificationsAre(
1029+ ["spec1", "spec2"], series_on_webservice.getAllSpecifications())
1030+
1031+ def test_productseries_getValidSpecifications(self):
1032+ product = self.makeProduct()
1033+ productseries = self.factory.makeProductSeries(
1034+ product=product, name="fooix-dev")
1035+ self.factory.makeSpecification(
1036+ product=product, name="spec1", productseries=productseries,
1037+ goalstatus=SpecificationGoalStatus.ACCEPTED)
1038+ self.factory.makeSpecification(
1039+ goalstatus=SpecificationGoalStatus.DECLINED,
1040+ product=product, name="spec2", productseries=productseries)
1041+ self.factory.makeSpecification(
1042+ product=product, name="spec3", productseries=productseries,
1043+ goalstatus=SpecificationGoalStatus.ACCEPTED,
1044+ definition_status=SpecificationDefinitionStatus.OBSOLETE)
1045+ self.factory.makeSpecification(product=product, name="spec4")
1046+ product_on_webservice = self.getPillarOnWebservice(product)
1047+ series_on_webservice = product_on_webservice.getSeries(
1048+ name="fooix-dev")
1049+ # Should this be different to the results for distroseries?
1050+ self.assertNamesOfSpecificationsAre(
1051+ ["spec1", "spec2", "spec3"],
1052+ series_on_webservice.getAllSpecifications())
1053+
1054+ def test_projectgroup_getAllSpecifications(self):
1055+ productgroup = self.factory.makeProject()
1056+ other_productgroup = self.factory.makeProject()
1057+ product1 = self.factory.makeProduct(project=productgroup)
1058+ product2 = self.factory.makeProduct(project=productgroup)
1059+ product3 = self.factory.makeProduct(project=other_productgroup)
1060+ self.factory.makeSpecification(
1061+ product=product1, name="spec1")
1062+ self.factory.makeSpecification(
1063+ product=product2, name="spec2",
1064+ definition_status=SpecificationDefinitionStatus.OBSOLETE)
1065+ self.factory.makeSpecification(
1066+ product=product3, name="spec3")
1067+ product_on_webservice = self.getPillarOnWebservice(productgroup)
1068+ # Should this be different to the results for distroseries?
1069+ self.assertNamesOfSpecificationsAre(
1070+ ["spec1", "spec2"],
1071+ product_on_webservice.getAllSpecifications())
1072+
1073+ def test_projectgroup_getValidSpecifications(self):
1074+ productgroup = self.factory.makeProject()
1075+ other_productgroup = self.factory.makeProject()
1076+ product1 = self.factory.makeProduct(project=productgroup)
1077+ product2 = self.factory.makeProduct(project=productgroup)
1078+ product3 = self.factory.makeProduct(project=other_productgroup)
1079+ self.factory.makeSpecification(
1080+ product=product1, name="spec1")
1081+ self.factory.makeSpecification(
1082+ product=product2, name="spec2",
1083+ definition_status=SpecificationDefinitionStatus.OBSOLETE)
1084+ self.factory.makeSpecification(
1085+ product=product3, name="spec3")
1086+ product_on_webservice = self.getPillarOnWebservice(productgroup)
1087+ # Should this be different to the results for distroseries?
1088+ self.assertNamesOfSpecificationsAre(
1089+ ["spec1", "spec2"],
1090+ product_on_webservice.getValidSpecifications())
1091+
1092+ def test_person_getAllSpecifications(self):
1093+ person = self.factory.makePerson(name="james-w")
1094+ product = self.factory.makeProduct()
1095+ self.factory.makeSpecification(
1096+ product=product, name="spec1", drafter=person)
1097+ self.factory.makeSpecification(
1098+ product=product, name="spec2", approver=person,
1099+ definition_status=SpecificationDefinitionStatus.OBSOLETE)
1100+ self.factory.makeSpecification(
1101+ product=product, name="spec3")
1102+ launchpadlib = self.getLaunchpadlib()
1103+ person_on_webservice = launchpadlib.load(
1104+ str(launchpadlib._root_uri) + '/~james-w')
1105+ self.assertNamesOfSpecificationsAre(
1106+ ["spec1", "spec2"], person_on_webservice.getAllSpecifications())
1107+
1108+ def test_person_getValidSpecifications(self):
1109+ person = self.factory.makePerson(name="james-w")
1110+ product = self.factory.makeProduct()
1111+ self.factory.makeSpecification(
1112+ product=product, name="spec1", drafter=person)
1113+ self.factory.makeSpecification(
1114+ product=product, name="spec2", approver=person,
1115+ definition_status=SpecificationDefinitionStatus.OBSOLETE)
1116+ self.factory.makeSpecification(
1117+ product=product, name="spec3")
1118+ launchpadlib = self.getLaunchpadlib()
1119+ person_on_webservice = launchpadlib.load(
1120+ str(launchpadlib._root_uri) + '/~james-w')
1121+ self.assertNamesOfSpecificationsAre(
1122+ ["spec1", "spec2"], person_on_webservice.getAllSpecifications())
1123
1124=== modified file 'lib/lp/testing/factory.py'
1125--- lib/lp/testing/factory.py 2010-07-14 19:33:16 +0000
1126+++ lib/lp/testing/factory.py 2010-07-15 16:19:46 +0000
1127@@ -67,7 +67,8 @@
1128 from lp.archiveuploader.dscfile import DSCFile
1129 from lp.archiveuploader.uploadpolicy import BuildDaemonUploadPolicy
1130 from lp.blueprints.interfaces.specification import (
1131- ISpecificationSet, SpecificationDefinitionStatus)
1132+ ISpecificationSet, SpecificationDefinitionStatus, SpecificationGoalStatus,
1133+ SpecificationPriority)
1134 from lp.blueprints.interfaces.sprint import ISprintSet
1135
1136 from lp.bugs.interfaces.bug import CreateBugParams, IBugSet
1137@@ -1369,7 +1370,11 @@
1138 mail.parsed_string = mail.as_string()
1139 return mail
1140
1141- def makeSpecification(self, product=None, title=None, distribution=None):
1142+ def makeSpecification(self, product=None, title=None, distribution=None,
1143+ name=None, specurl=None, summary=None, definition_status=None,
1144+ assignee=None, drafter=None, approver=None, priority=None,
1145+ owner=None, goalstatus=None, whiteboard=None, productseries=None,
1146+ distroseries=None, milestone=None):
1147 """Create and return a new, arbitrary Blueprint.
1148
1149 :param product: The product to make the blueprint on. If one is
1150@@ -1379,15 +1384,58 @@
1151 product = self.makeProduct()
1152 if title is None:
1153 title = self.getUniqueString('title')
1154+ if name is None:
1155+ name = self.getUniqueString('name')
1156+ if summary is None:
1157+ summary = self.getUniqueString('summary')
1158+ if definition_status is None:
1159+ definition_status = SpecificationDefinitionStatus.NEW
1160+ if priority is None:
1161+ priority = SpecificationPriority.LOW
1162+ if goalstatus is None:
1163+ goalstatus = SpecificationGoalStatus.PROPOSED
1164+ if owner is None:
1165+ owner = self.makePerson()
1166+ goal_proposer = None
1167+ date_goal_proposed = None
1168+ if distroseries is not None or productseries is not None:
1169+ goal_proposer = self.makePerson()
1170+ date_goal_proposed = datetime.now(pytz.UTC)
1171+ goal_decider = None
1172+ date_goal_decided = None
1173+ if goalstatus != SpecificationGoalStatus.PROPOSED:
1174+ goal_decider = self.makePerson()
1175+ date_goal_decided = datetime.now(pytz.UTC)
1176+ completer = None
1177+ date_completed = None
1178+ if definition_status == SpecificationDefinitionStatus.OBSOLETE:
1179+ completer = self.makePerson()
1180+ date_completed = datetime.now(pytz.UTC)
1181 return getUtility(ISpecificationSet).new(
1182- name=self.getUniqueString('name'),
1183+ name=name,
1184 title=title,
1185- specurl=None,
1186- summary=self.getUniqueString('summary'),
1187- definition_status=SpecificationDefinitionStatus.NEW,
1188- owner=self.makePerson(),
1189+ specurl=specurl,
1190+ summary=summary,
1191+ definition_status=definition_status,
1192+ owner=owner,
1193 product=product,
1194- distribution=distribution)
1195+ productseries=productseries,
1196+ distribution=distribution,
1197+ distroseries=distroseries,
1198+ assignee=assignee,
1199+ drafter=drafter,
1200+ approver=approver,
1201+ priority=priority,
1202+ goalstatus=goalstatus,
1203+ whiteboard=whiteboard,
1204+ goal_proposer=goal_proposer,
1205+ date_goal_proposed=date_goal_proposed,
1206+ milestone=milestone,
1207+ date_completed=date_completed,
1208+ completer=completer,
1209+ goal_decider=goal_decider,
1210+ date_goal_decided=date_goal_decided,
1211+ )
1212
1213 def makeQuestion(self, target=None, title=None):
1214 """Create and return a new, arbitrary Question.