Merge lp:~james-w/launchpad/expose-blueprints into lp:launchpad
- expose-blueprints
- Merge into devel
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 | ||||
Related bugs: |
|
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 getValidSpecifi
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-
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/
336: E222 multiple spaces after operator
375: E222 multiple spaces after operator
681: E231 missing whitespace after ','
./lib/lp/
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/
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.
James Westby (james-w) wrote : | # |
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.
> not ISpecification.
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 updateLifecycle
> 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 updateLifecycle
> 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 updateLifecycle
> 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 ...
Leonard Richardson (leonardr) wrote : | # |
> > 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(
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 updateLifecycle
> > 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 updateLifecycle
> > 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 updateLifecycle
> > 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....
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(
> 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 getSpecOnWebser
> ...
> pillar = launchpadlib.
> pillar_name)
> return pillar.
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/
> 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._
>
> 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 WebServiceClien
> 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
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 ISpecificationT
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.
* SpecificationRe
* updateLifecycle
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 ;)
Guilherme Salgado (salgado) wrote : | # |
https:/
I've also filed a bug for getting rid of the sole remaining call to updateLifecycle
Preview Diff
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. |
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 goalstatus but goal.
useful, but I wonder why you exported ISpecification.
not ISpecification.
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 updateLifecycle Status 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 updateLifecycle Status( ) returns None if the Status( ) call with
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 updateLifecycle
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 et.new( ) to take the same arguments, which seems like
new arguments, which is fine, but you also changed
ISpecificationS
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 IHasSpecificati onsTests, you could save a lot of setup code by
writing a helper methods that ta...