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
=== modified file 'lib/canonical/launchpad/doc/tales.txt'
--- lib/canonical/launchpad/doc/tales.txt 2010-05-21 14:58:50 +0000
+++ lib/canonical/launchpad/doc/tales.txt 2010-07-15 16:19:46 +0000
@@ -669,8 +669,11 @@
669Blueprints669Blueprints
670..........670..........
671671
672 >>> from lp.blueprints.interfaces.specification import (
673 ... SpecificationPriority)
672 >>> login('test@canonical.com')674 >>> login('test@canonical.com')
673 >>> specification = factory.makeSpecification()675 >>> specification = factory.makeSpecification(
676 ... priority=SpecificationPriority.UNDEFINED)
674 >>> test_tales("specification/fmt:link", specification=specification)677 >>> test_tales("specification/fmt:link", specification=specification)
675 u'<a...class="sprite blueprint-undefined">...</a>'678 u'<a...class="sprite blueprint-undefined">...</a>'
676679
@@ -678,7 +681,8 @@
678Blueprint branches681Blueprint branches
679..................682..................
680683
681 >>> specification = factory.makeSpecification()684 >>> specification = factory.makeSpecification(
685 ... priority=SpecificationPriority.UNDEFINED)
682 >>> branch = factory.makeAnyBranch()686 >>> branch = factory.makeAnyBranch()
683 >>> specification_branch = specification.linkBranch(branch, branch.owner)687 >>> specification_branch = specification.linkBranch(branch, branch.owner)
684 >>> test_tales("specification_branch/fmt:link",688 >>> test_tales("specification_branch/fmt:link",
685689
=== modified file 'lib/canonical/launchpad/interfaces/_schema_circular_imports.py'
--- lib/canonical/launchpad/interfaces/_schema_circular_imports.py 2010-07-13 15:29:08 +0000
+++ lib/canonical/launchpad/interfaces/_schema_circular_imports.py 2010-07-15 16:19:46 +0000
@@ -43,6 +43,8 @@
43from lp.blueprints.interfaces.specification import ISpecification43from lp.blueprints.interfaces.specification import ISpecification
44from lp.blueprints.interfaces.specificationbranch import (44from lp.blueprints.interfaces.specificationbranch import (
45 ISpecificationBranch)45 ISpecificationBranch)
46from lp.blueprints.interfaces.specificationtarget import (
47 IHasSpecifications, ISpecificationTarget)
46from lp.code.interfaces.branch import IBranch48from lp.code.interfaces.branch import IBranch
47from lp.code.interfaces.branchmergeproposal import IBranchMergeProposal49from lp.code.interfaces.branchmergeproposal import IBranchMergeProposal
48from lp.code.interfaces.branchsubscription import IBranchSubscription50from lp.code.interfaces.branchsubscription import IBranchSubscription
@@ -433,3 +435,13 @@
433435
434# IProductSeries436# IProductSeries
435patch_reference_property(IProductSeries, 'product', IProduct)437patch_reference_property(IProductSeries, 'product', IProduct)
438
439# IHasSpecifications
440patch_collection_return_type(
441 IHasSpecifications, 'getAllSpecifications', ISpecification)
442patch_collection_return_type(
443 IHasSpecifications, 'getValidSpecifications', ISpecification)
444
445# ISpecificationTarget
446patch_entry_return_type(
447 ISpecificationTarget, 'getSpecification', ISpecification)
436448
=== modified file 'lib/lp/blueprints/interfaces/specification.py'
--- lib/lp/blueprints/interfaces/specification.py 2010-02-19 12:05:10 +0000
+++ lib/lp/blueprints/interfaces/specification.py 2010-07-15 16:19:46 +0000
@@ -22,32 +22,36 @@
22 'SpecificationImplementationStatus',22 'SpecificationImplementationStatus',
23 'SpecificationLifecycleStatus',23 'SpecificationLifecycleStatus',
24 'SpecificationPriority',24 'SpecificationPriority',
25 'SpecificationSort'25 'SpecificationSort',
26 ]26 ]
2727
2828
29from lazr.restful.declarations import (29from lazr.restful.declarations import (
30 REQUEST_USER, call_with, export_as_webservice_entry,30 exported, export_as_webservice_entry)
31 export_write_operation, operation_parameters, operation_returns_entry)31from lazr.restful.fields import ReferenceChoice
32from lazr.restful.fields import Reference
33from zope.interface import Interface, Attribute32from zope.interface import Interface, Attribute
34from zope.component import getUtility33from zope.component import getUtility
3534
36from zope.schema import Datetime, Int, Choice, Text, TextLine, Bool35from zope.schema import Datetime, Int, Choice, Text, TextLine, Bool
3736
38from canonical.launchpad import _37from canonical.launchpad import _
38from canonical.launchpad.interfaces.validation import valid_webref
39from canonical.launchpad.fields import (39from canonical.launchpad.fields import (
40 ContentNameField, PublicPersonChoice, Summary, Title)40 ContentNameField, PublicPersonChoice, Summary, Title)
41from canonical.launchpad.validators import LaunchpadValidationError41from canonical.launchpad.validators import LaunchpadValidationError
42from lp.registry.interfaces.role import IHasOwner42from lp.blueprints.interfaces.sprint import ISprint
43from lp.code.interfaces.branch import IBranch43from lp.blueprints.interfaces.specificationtarget import (
44 IHasSpecifications)
44from lp.code.interfaces.branchlink import IHasLinkedBranches45from lp.code.interfaces.branchlink import IHasLinkedBranches
46from lp.registry.interfaces.distribution import IDistribution
47from lp.registry.interfaces.distroseries import IDistroSeries
45from lp.registry.interfaces.mentoringoffer import ICanBeMentored48from lp.registry.interfaces.mentoringoffer import ICanBeMentored
46from canonical.launchpad.interfaces.validation import valid_webref49from lp.registry.interfaces.milestone import IMilestone
47from lp.registry.interfaces.projectgroup import IProjectGroup50from lp.registry.interfaces.projectgroup import IProjectGroup
48from lp.blueprints.interfaces.sprint import ISprint51from lp.registry.interfaces.product import IProduct
49from lp.blueprints.interfaces.specificationtarget import (52from lp.registry.interfaces.productseries import IProductSeries
50 IHasSpecifications)53from lp.registry.interfaces.role import IHasOwner
54
5155
52from lazr.enum import (56from lazr.enum import (
53 DBEnumeratedType, DBItem, EnumeratedType, Item)57 DBEnumeratedType, DBItem, EnumeratedType, Item)
@@ -125,7 +129,8 @@
125 GOOD = DBItem(70, """129 GOOD = DBItem(70, """
126 Good progress130 Good progress
127131
128 The feature is considered on track for delivery in the targeted release.132 The feature is considered on track for delivery in the targeted
133 release.
129 """)134 """)
130135
131 BETA = DBItem(75, """136 BETA = DBItem(75, """
@@ -148,8 +153,8 @@
148 AWAITINGDEPLOYMENT = DBItem(85, """153 AWAITINGDEPLOYMENT = DBItem(85, """
149 Deployment154 Deployment
150155
151 The implementation has been done, and can be deployed in the production156 The implementation has been done, and can be deployed in the
152 environment, but this has not yet been done by the system157 production environment, but this has not yet been done by the system
153 administrators. (This status is typically used for Web services where158 administrators. (This status is typically used for Web services where
154 code is not released but instead is pushed into production.159 code is not released but instead is pushed into production.
155 """)160 """)
@@ -441,8 +446,8 @@
441 NEW = DBItem(40, """446 NEW = DBItem(40, """
442 New447 New
443448
444 No thought has yet been given to implementation strategy, dependencies,449 No thought has yet been given to implementation strategy,
445 or presentation/UI issues.450 dependencies, or presentation/UI issues.
446 """)451 """)
447452
448 SUPERSEDED = DBItem(60, """453 SUPERSEDED = DBItem(60, """
@@ -554,49 +559,58 @@
554class INewSpecification(Interface):559class INewSpecification(Interface):
555 """A schema for a new specification."""560 """A schema for a new specification."""
556561
557 name = SpecNameField(562 name = exported(
558 title=_('Name'), required=True, readonly=False,563 SpecNameField(
559 description=_(564 title=_('Name'), required=True, readonly=False,
560 "May contain lower-case letters, numbers, and dashes. "565 description=_(
561 "It will be used in the specification url. "566 "May contain lower-case letters, numbers, and dashes. "
562 "Examples: mozilla-type-ahead-find, postgres-smart-serial.")567 "It will be used in the specification url. "
563 )568 "Examples: mozilla-type-ahead-find, postgres-smart-serial.")))
564 title = Title(569 title = exported(
565 title=_('Title'), required=True, description=_(570 Title(
566 "Describe the feature as clearly as possible in up to 70 "571 title=_('Title'), required=True, description=_(
567 "characters. This title is displayed in every feature "572 "Describe the feature as clearly as possible in up to 70 "
568 "list or report."))573 "characters. This title is displayed in every feature "
569 specurl = SpecURLField(574 "list or report.")))
570 title=_('Specification URL'), required=False,575 specurl = exported(
571 description=_(576 SpecURLField(
572 "The URL of the specification. This is usually a wiki page."),577 title=_('Specification URL'), required=False,
573 constraint=valid_webref)578 description=_(
574 summary = Summary(579 "The URL of the specification. This is usually a wiki page."),
575 title=_('Summary'), required=True, description=_(580 constraint=valid_webref),
576 "A single-paragraph description of the feature. "581 exported_as="specification_url")
577 "This will also be displayed in most feature listings."))582 summary = exported(
578 definition_status = Choice(583 Summary(
579 title=_('Definition Status'),584 title=_('Summary'), required=True, description=_(
580 vocabulary=SpecificationDefinitionStatus,585 "A single-paragraph description of the feature. "
581 default=SpecificationDefinitionStatus.NEW,586 "This will also be displayed in most feature listings.")))
582 description=_(587 definition_status = exported(
583 "The current status of the process to define the "588 Choice(
584 "feature and get approval for the implementation plan."))589 title=_('Definition Status'),
585 assignee = PublicPersonChoice(590 vocabulary=SpecificationDefinitionStatus,
586 title=_('Assignee'), required=False,591 default=SpecificationDefinitionStatus.NEW,
587 description=_("The person responsible for implementing the feature."),592 description=_(
588 vocabulary='ValidPersonOrTeam')593 "The current status of the process to define the "
589 drafter = PublicPersonChoice(594 "feature and get approval for the implementation plan.")))
590 title=_('Drafter'), required=False,595 assignee = exported(
591 description=_(596 PublicPersonChoice(
592 "The person responsible for drafting the specification."),597 title=_('Assignee'), required=False,
593 vocabulary='ValidPersonOrTeam')598 description=_(
594 approver = PublicPersonChoice(599 "The person responsible for implementing the feature."),
595 title=_('Approver'), required=False,600 vocabulary='ValidPersonOrTeam'))
596 description=_(601 drafter = exported(
597 "The person responsible for approving the specification, "602 PublicPersonChoice(
598 "and for reviewing the code when it's ready to be landed."),603 title=_('Drafter'), required=False,
599 vocabulary='ValidPersonOrTeam')604 description=_(
605 "The person responsible for drafting the specification."),
606 vocabulary='ValidPersonOrTeam'))
607 approver = exported(
608 PublicPersonChoice(
609 title=_('Approver'), required=False,
610 description=_(
611 "The person responsible for approving the specification, "
612 "and for reviewing the code when it's ready to be landed."),
613 vocabulary='ValidPersonOrTeam'))
600614
601615
602class INewSpecificationProjectTarget(Interface):616class INewSpecificationProjectTarget(Interface):
@@ -645,7 +659,10 @@
645659
646class ISpecification(INewSpecification, INewSpecificationTarget, IHasOwner,660class ISpecification(INewSpecification, INewSpecificationTarget, IHasOwner,
647 ICanBeMentored, IHasLinkedBranches):661 ICanBeMentored, IHasLinkedBranches):
648 """A Specification."""662 """A Specification.
663
664 Also known as a blueprint.
665 """
649666
650 export_as_webservice_entry()667 export_as_webservice_entry()
651668
@@ -655,46 +672,62 @@
655 # referencing it.672 # referencing it.
656 id = Int(title=_("Database ID"), required=True, readonly=True)673 id = Int(title=_("Database ID"), required=True, readonly=True)
657674
658 priority = Choice(675 priority = exported(
659 title=_('Priority'), vocabulary=SpecificationPriority,676 Choice(
660 default=SpecificationPriority.UNDEFINED, required=True)677 title=_('Priority'), vocabulary=SpecificationPriority,
661 datecreated = Datetime(678 default=SpecificationPriority.UNDEFINED, required=True))
662 title=_('Date Created'), required=True, readonly=True)679 datecreated = exported(
663 owner = PublicPersonChoice(680 Datetime(
664 title=_('Owner'), required=True, readonly=True,681 title=_('Date Created'), required=True, readonly=True),
665 vocabulary='ValidPersonOrTeam')682 exported_as='date_created')
683 owner = exported(
684 PublicPersonChoice(
685 title=_('Owner'), required=True, readonly=True,
686 vocabulary='ValidPersonOrTeam'))
666 # target687 # target
667 product = Choice(title=_('Project'), required=False,688 product = exported(
668 vocabulary='Product')689 ReferenceChoice(title=_('Project'), required=False,
669 distribution = Choice(title=_('Distribution'), required=False,690 vocabulary='Product', schema=IProduct),
670 vocabulary='Distribution')691 exported_as='project')
692 distribution = exported(
693 ReferenceChoice(title=_('Distribution'), required=False,
694 vocabulary='Distribution', schema=IDistribution))
671695
672 # series696 # series
673 productseries = Choice(title=_('Series Goal'), required=False,697 productseries = exported(
674 vocabulary='FilteredProductSeries',698 ReferenceChoice(title=_('Series Goal'), required=False,
675 description=_(699 vocabulary='FilteredProductSeries',
676 "Choose a series in which you would like to deliver "700 description=_(
677 "this feature. Selecting '(no value)' will clear the goal."))701 "Choose a series in which you would like to deliver "
678 distroseries = Choice(title=_('Series Goal'), required=False,702 "this feature. Selecting '(no value)' will clear the goal."),
679 vocabulary='FilteredDistroSeries',703 schema=IProductSeries),
680 description=_(704 exported_as='project_series')
681 "Choose a series in which you would like to deliver "705 distroseries = exported(
682 "this feature. Selecting '(no value)' will clear the goal."))706 ReferenceChoice(title=_('Series Goal'), required=False,
707 vocabulary='FilteredDistroSeries',
708 description=_(
709 "Choose a series in which you would like to deliver "
710 "this feature. Selecting '(no value)' will clear the goal."),
711 schema=IDistroSeries))
683712
684 # milestone713 # milestone
685 milestone = Choice(714 milestone = exported(
686 title=_('Milestone'), required=False, vocabulary='Milestone',715 ReferenceChoice(
687 description=_(716 title=_('Milestone'), required=False, vocabulary='Milestone',
688 "The milestone in which we would like this feature to be "717 description=_(
689 "delivered."))718 "The milestone in which we would like this feature to be "
719 "delivered."),
720 schema=IMilestone))
690721
691 # nomination to a series for release management722 # nomination to a series for release management
692 goal = Attribute("The series for which this feature is a goal.")723 goal = Attribute("The series for which this feature is a goal.")
693 goalstatus = Choice(724 goalstatus = exported(
694 title=_('Goal Acceptance'), vocabulary=SpecificationGoalStatus,725 Choice(
695 default=SpecificationGoalStatus.PROPOSED, description=_(726 title=_('Goal Acceptance'), vocabulary=SpecificationGoalStatus,
696 "Whether or not the drivers have accepted this feature as "727 default=SpecificationGoalStatus.PROPOSED, description=_(
697 "a goal for the targeted series."))728 "Whether or not the drivers have accepted this feature as "
729 "a goal for the targeted series.")),
730 exported_as='goal_status')
698 goal_proposer = Attribute("The person who nominated the spec for "731 goal_proposer = Attribute("The person who nominated the spec for "
699 "this series.")732 "this series.")
700 date_goal_proposed = Attribute("The date of the nomination.")733 date_goal_proposed = Attribute("The date of the nomination.")
@@ -703,10 +736,11 @@
703 date_goal_decided = Attribute("The date the spec was approved "736 date_goal_decided = Attribute("The date the spec was approved "
704 "or declined as a goal.")737 "or declined as a goal.")
705738
706 whiteboard = Text(title=_('Status Whiteboard'), required=False,739 whiteboard = exported(
707 description=_(740 Text(title=_('Status Whiteboard'), required=False,
708 "Any notes on the status of this spec you would like to make. "741 description=_(
709 "Your changes will override the current text."))742 "Any notes on the status of this spec you would like to "
743 "make. Your changes will override the current text.")))
710 direction_approved = Bool(title=_('Basic direction approved?'),744 direction_approved = Bool(title=_('Basic direction approved?'),
711 required=False, default=False, description=_("Check this to "745 required=False, default=False, description=_("Check this to "
712 "indicate that the drafter and assignee have satisfied the "746 "indicate that the drafter and assignee have satisfied the "
@@ -880,7 +914,6 @@
880 """Return the SpecificationBranch link for the branch, or None."""914 """Return the SpecificationBranch link for the branch, or None."""
881915
882916
883# Interfaces for containers
884class ISpecificationSet(IHasSpecifications):917class ISpecificationSet(IHasSpecifications):
885 """A container for specifications."""918 """A container for specifications."""
886919
887920
=== modified file 'lib/lp/blueprints/interfaces/specificationtarget.py'
--- lib/lp/blueprints/interfaces/specificationtarget.py 2009-08-20 14:00:52 +0000
+++ lib/lp/blueprints/interfaces/specificationtarget.py 2010-07-15 16:19:46 +0000
@@ -14,6 +14,12 @@
14 ]14 ]
1515
16from zope.interface import Interface, Attribute16from zope.interface import Interface, Attribute
17from zope.schema import TextLine
18
19from canonical.launchpad import _
20from lazr.restful.declarations import (
21 export_read_operation, operation_parameters,
22 operation_returns_collection_of, operation_returns_entry)
1723
1824
19class IHasSpecifications(Interface):25class IHasSpecifications(Interface):
@@ -59,6 +65,15 @@
59 situations in which these are not rendered.65 situations in which these are not rendered.
60 """66 """
6167
68 @operation_returns_collection_of(Interface) # really ISpecification
69 @export_read_operation()
70 def getAllSpecifications():
71 """Return all the specifications associated with this object."""
72
73 @operation_returns_collection_of(Interface) # really ISpecification
74 @export_read_operation()
75 def getValidSpecifications():
76 """Return all the non-obsolete specifications for this object."""
6277
6378
64class ISpecificationTarget(IHasSpecifications):79class ISpecificationTarget(IHasSpecifications):
@@ -66,6 +81,10 @@
66 specifications directly attached to them.81 specifications directly attached to them.
67 """82 """
6883
84 @operation_parameters(
85 name=TextLine(title=_('The name of the specification')))
86 @operation_returns_entry(Interface) # really ISpecification
87 @export_read_operation()
69 def getSpecification(name):88 def getSpecification(name):
70 """Returns the specification with the given name, for this target,89 """Returns the specification with the given name, for this target,
71 or None.90 or None.
7291
=== modified file 'lib/lp/blueprints/model/specification.py'
--- lib/lp/blueprints/model/specification.py 2009-09-21 14:56:07 +0000
+++ lib/lp/blueprints/model/specification.py 2010-07-15 16:19:46 +0000
@@ -229,7 +229,7 @@
229 # and make sure there is no leftover distroseries goal229 # and make sure there is no leftover distroseries goal
230 self.productseries = None230 self.productseries = None
231 else:231 else:
232 raise AssertionError, 'Inappropriate goal.'232 raise AssertionError('Inappropriate goal.')
233 # record who made the proposal, and when233 # record who made the proposal, and when
234 self.goal_proposer = proposer234 self.goal_proposer = proposer
235 self.date_goal_proposed = UTC_NOW235 self.date_goal_proposed = UTC_NOW
@@ -685,6 +685,14 @@
685 """See IHasSpecifications."""685 """See IHasSpecifications."""
686 return self.specifications(filter=[SpecificationFilter.ALL]).count()686 return self.specifications(filter=[SpecificationFilter.ALL]).count()
687687
688 def getAllSpecifications(self):
689 """See IHasSpecifications."""
690 return self.all_specifications
691
692 def getValidSpecifications(self):
693 """See IHasSpecifications."""
694 return self.valid_specifications
695
688696
689class SpecificationSet(HasSpecificationsMixin):697class SpecificationSet(HasSpecificationsMixin):
690 """The set of feature specifications."""698 """The set of feature specifications."""
@@ -790,7 +798,7 @@
790798
791 # filter based on completion. see the implementation of799 # filter based on completion. see the implementation of
792 # Specification.is_complete() for more details800 # Specification.is_complete() for more details
793 completeness = Specification.completeness_clause801 completeness = Specification.completeness_clause
794802
795 if SpecificationFilter.COMPLETE in filter:803 if SpecificationFilter.COMPLETE in filter:
796 query += ' AND ( %s ) ' % completeness804 query += ' AND ( %s ) ' % completeness
@@ -838,13 +846,23 @@
838 def new(self, name, title, specurl, summary, definition_status,846 def new(self, name, title, specurl, summary, definition_status,
839 owner, approver=None, product=None, distribution=None, assignee=None,847 owner, approver=None, product=None, distribution=None, assignee=None,
840 drafter=None, whiteboard=None,848 drafter=None, whiteboard=None,
841 priority=SpecificationPriority.UNDEFINED):849 priority=SpecificationPriority.UNDEFINED,
850 goalstatus=SpecificationGoalStatus.PROPOSED,
851 productseries=None, distroseries=None,
852 goal_proposer=None, date_goal_proposed=None, milestone=None,
853 date_completed=None, completer=None, goal_decider=None,
854 date_goal_decided=None):
842 """See ISpecificationSet."""855 """See ISpecificationSet."""
843 return Specification(name=name, title=title, specurl=specurl,856 return Specification(name=name, title=title, specurl=specurl,
844 summary=summary, priority=priority,857 summary=summary, priority=priority,
845 definition_status=definition_status, owner=owner,858 definition_status=definition_status, owner=owner,
846 approver=approver, product=product, distribution=distribution,859 approver=approver, product=product, distribution=distribution,
847 assignee=assignee, drafter=drafter, whiteboard=whiteboard)860 assignee=assignee, drafter=drafter, whiteboard=whiteboard,
861 goalstatus=goalstatus, productseries=productseries,
862 distroseries=distroseries, goal_proposer=goal_proposer,
863 date_goal_proposed=date_goal_proposed, milestone=milestone,
864 date_completed=date_completed, completer=completer,
865 goal_decider=goal_decider, date_goal_decided=date_goal_decided)
848866
849 def getDependencyDict(self, specifications):867 def getDependencyDict(self, specifications):
850 """See `ISpecificationSet`."""868 """See `ISpecificationSet`."""
@@ -859,7 +877,8 @@
859 FROM SpecificationDependency, Specification877 FROM SpecificationDependency, Specification
860 WHERE SpecificationDependency.specification IN %s878 WHERE SpecificationDependency.specification IN %s
861 AND SpecificationDependency.dependency = Specification.id879 AND SpecificationDependency.dependency = Specification.id
862 ORDER BY Specification.priority DESC, Specification.name, Specification.id880 ORDER BY Specification.priority DESC, Specification.name,
881 Specification.id
863 """ % sqlvalues(specification_ids)).get_all()882 """ % sqlvalues(specification_ids)).get_all()
864883
865 dependencies = {}884 dependencies = {}
866885
=== modified file 'lib/lp/blueprints/stories/standalone/sprint-links.txt'
--- lib/lp/blueprints/stories/standalone/sprint-links.txt 2009-09-22 10:48:09 +0000
+++ lib/lp/blueprints/stories/standalone/sprint-links.txt 2010-07-15 16:19:46 +0000
@@ -13,10 +13,6 @@
13 >>> browser.open('http://blueprints.launchpad.dev/firefox/+spec/canvas')13 >>> browser.open('http://blueprints.launchpad.dev/firefox/+spec/canvas')
14 >>> browser.isHtml14 >>> browser.isHtml
15 True15 True
16 >>> 'Accepted' in browser.contents # make sure the page is not polluted
17 False
18 >>> 'Proposed' in browser.contents # make sure the page is not polluted
19 False
2016
21Then we are going to propose it for the meeting agenda:17Then we are going to propose it for the meeting agenda:
2218
2319
=== added file 'lib/lp/blueprints/tests/test_implements.py'
--- lib/lp/blueprints/tests/test_implements.py 1970-01-01 00:00:00 +0000
+++ lib/lp/blueprints/tests/test_implements.py 2010-07-15 16:19:46 +0000
@@ -0,0 +1,61 @@
1# Copyright 2009 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Tests that various objects implement specification-related interfaces."""
5
6__metaclass__ = type
7
8from canonical.testing import DatabaseFunctionalLayer
9from lp.blueprints.interfaces.specificationtarget import (
10 IHasSpecifications, ISpecificationTarget)
11from lp.testing import TestCaseWithFactory
12
13
14class ImplementsIHasSpecificationsTests(TestCaseWithFactory):
15 """Test that various objects implement IHasSpecifications."""
16 layer = DatabaseFunctionalLayer
17
18 def test_product_implements_IHasSpecifications(self):
19 product = self.factory.makeProduct()
20 self.assertProvides(product, IHasSpecifications)
21
22 def test_distribution_implements_IHasSpecifications(self):
23 product = self.factory.makeProduct()
24 self.assertProvides(product, IHasSpecifications)
25
26 def test_projectgroup_implements_IHasSpecifications(self):
27 projectgroup = self.factory.makeProject()
28 self.assertProvides(projectgroup, IHasSpecifications)
29
30 def test_person_implements_IHasSpecifications(self):
31 person = self.factory.makePerson()
32 self.assertProvides(person, IHasSpecifications)
33
34 def test_productseries_implements_IHasSpecifications(self):
35 productseries = self.factory.makeProductSeries()
36 self.assertProvides(productseries, IHasSpecifications)
37
38 def test_distroseries_implements_IHasSpecifications(self):
39 distroseries = self.factory.makeDistroSeries()
40 self.assertProvides(distroseries, IHasSpecifications)
41
42
43class ImplementsISpecificationTargetTests(TestCaseWithFactory):
44 """Test that various objects implement ISpecificationTarget."""
45 layer = DatabaseFunctionalLayer
46
47 def test_product_implements_ISpecificationTarget(self):
48 product = self.factory.makeProduct()
49 self.assertProvides(product, ISpecificationTarget)
50
51 def test_distribution_implements_ISpecificationTarget(self):
52 product = self.factory.makeProduct()
53 self.assertProvides(product, ISpecificationTarget)
54
55 def test_productseries_implements_ISpecificationTarget(self):
56 productseries = self.factory.makeProductSeries()
57 self.assertProvides(productseries, ISpecificationTarget)
58
59 def test_distroseries_implements_ISpecificationTarget(self):
60 distroseries = self.factory.makeDistroSeries()
61 self.assertProvides(distroseries, ISpecificationTarget)
062
=== added file 'lib/lp/blueprints/tests/test_webservice.py'
--- lib/lp/blueprints/tests/test_webservice.py 1970-01-01 00:00:00 +0000
+++ lib/lp/blueprints/tests/test_webservice.py 2010-07-15 16:19:46 +0000
@@ -0,0 +1,550 @@
1# Copyright 2009 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Webservice unit tests related to Launchpad blueprints."""
5
6__metaclass__ = type
7
8from canonical.testing import DatabaseFunctionalLayer
9from canonical.launchpad.testing.pages import webservice_for_person
10from lp.blueprints.interfaces.specification import (
11 SpecificationDefinitionStatus, SpecificationGoalStatus,
12 SpecificationPriority)
13from lp.testing import (
14 launchpadlib_for, TestCaseWithFactory)
15
16
17class SpecificationWebserviceTestCase(TestCaseWithFactory):
18
19 def makeProduct(self):
20 return self.factory.makeProduct(name="fooix")
21
22 def makeDistribution(self):
23 return self.factory.makeDistribution(name="foobuntu")
24
25 def getLaunchpadlib(self):
26 user = self.factory.makePerson()
27 return launchpadlib_for("testing", user)
28
29 def getSpecOnWebservice(self, spec_object):
30 launchpadlib = self.getLaunchpadlib()
31 if spec_object.product is not None:
32 pillar_name = spec_object.product.name
33 else:
34 pillar_name = spec_object.distribution.name
35 return launchpadlib.load(
36 str(launchpadlib._root_uri) + '/%s/+spec/%s'
37 % (pillar_name, spec_object.name))
38
39 def getPillarOnWebservice(self, pillar_obj):
40 launchpadlib = self.getLaunchpadlib()
41 return launchpadlib.load(
42 str(launchpadlib._root_uri) + '/' + pillar_obj.name)
43
44
45class SpecificationAttributeWebserviceTests(SpecificationWebserviceTestCase):
46 """Test accessing specification attributes over the webservice."""
47 layer = DatabaseFunctionalLayer
48
49 def makeSimpleSpecification(self):
50 self.name = "some-spec"
51 self.title = "some-title"
52 self.url = "http://example.org/some_url"
53 self.summary = "Some summary."
54 definition_status = SpecificationDefinitionStatus.PENDINGAPPROVAL
55 self.definition_status = definition_status.title
56 self.assignee_name = "james-w"
57 assignee = self.factory.makePerson(name=self.assignee_name)
58 self.drafter_name = "jml"
59 drafter = self.factory.makePerson(name=self.drafter_name)
60 self.approver_name = "bob"
61 approver = self.factory.makePerson(name=self.approver_name)
62 self.owner_name = "mary"
63 owner = self.factory.makePerson(name=self.owner_name)
64 priority = SpecificationPriority.HIGH
65 self.priority = priority.title
66 goal_status = SpecificationGoalStatus.PROPOSED
67 self.goal_status = goal_status.title
68 self.whiteboard = "Some whiteboard"
69 product = self.factory.makeProduct()
70 return self.factory.makeSpecification(
71 product=product, name=self.name,
72 title=self.title, specurl=self.url,
73 summary=self.summary,
74 definition_status=definition_status,
75 assignee=assignee, drafter=drafter, approver=approver,
76 priority=priority,
77 owner=owner, whiteboard=self.whiteboard, goalstatus=goal_status)
78
79 def getSimpleSpecificationResponse(self):
80 self.spec_object = self.makeSimpleSpecification()
81 return self.getSpecOnWebservice(self.spec_object)
82
83 def test_can_retrieve_representation(self):
84 spec = self.makeSimpleSpecification()
85 user = self.factory.makePerson()
86 webservice = webservice_for_person(user)
87 response = webservice.get(
88 '/%s/+spec/%s' % (spec.product.name, spec.name))
89 self.assertEqual(response.status, 200)
90
91 def test_representation_contains_name(self):
92 spec = self.getSimpleSpecificationResponse()
93 self.assertEqual(self.name, spec.name)
94
95 def test_representation_contains_title(self):
96 spec = self.getSimpleSpecificationResponse()
97 self.assertEqual(self.title, spec.title)
98
99 def test_representation_contains_specification_url(self):
100 spec = self.getSimpleSpecificationResponse()
101 self.assertEqual(self.url, spec.specification_url)
102
103 def test_representation_contains_summary(self):
104 spec = self.getSimpleSpecificationResponse()
105 self.assertEqual(self.summary, spec.summary)
106
107 def test_representation_contains_definition_status(self):
108 spec = self.getSimpleSpecificationResponse()
109 self.assertEqual(
110 self.definition_status, spec.definition_status)
111
112 def test_representation_contains_assignee(self):
113 spec = self.getSimpleSpecificationResponse()
114 self.assertEqual(self.assignee_name, spec.assignee.name)
115
116 def test_representation_contains_drafter(self):
117 spec = self.getSimpleSpecificationResponse()
118 self.assertEqual(self.drafter_name, spec.drafter.name)
119
120 def test_representation_contains_approver(self):
121 spec = self.getSimpleSpecificationResponse()
122 self.assertEqual(self.approver_name, spec.approver.name)
123
124 def test_representation_contains_owner(self):
125 spec = self.getSimpleSpecificationResponse()
126 self.assertEqual(self.owner_name, spec.owner.name)
127
128 def test_representation_contains_priority(self):
129 spec = self.getSimpleSpecificationResponse()
130 self.assertEqual(self.priority, spec.priority)
131
132 def test_representation_contains_date_created(self):
133 spec = self.getSimpleSpecificationResponse()
134 self.assertEqual(self.spec_object.datecreated, spec.date_created)
135
136 def test_representation_contains_goal_status(self):
137 spec = self.getSimpleSpecificationResponse()
138 self.assertEqual(self.goal_status, spec.goal_status)
139
140 def test_representation_contains_whiteboard(self):
141 spec = self.getSimpleSpecificationResponse()
142 self.assertEqual(self.whiteboard, spec.whiteboard)
143
144 def test_representation_with_no_whiteboard(self):
145 product = self.makeProduct()
146 name = "some-spec"
147 spec_object = self.factory.makeSpecification(
148 product=product, name=name, whiteboard=None)
149 # Check that the factory didn't add a whiteboard
150 self.assertEqual(None, spec_object.whiteboard)
151 spec = self.getSpecOnWebservice(spec_object)
152 # Check that it is None on the webservice too
153 self.assertEqual(None, spec.whiteboard)
154
155 def test_representation_with_no_approver(self):
156 product = self.makeProduct()
157 name = "some-spec"
158 spec_object = self.factory.makeSpecification(
159 product=product, name=name, approver=None)
160 # Check that the factory didn't add an approver
161 self.assertEqual(None, spec_object.approver)
162 spec = self.getSpecOnWebservice(spec_object)
163 # Check that it is None on the webservice too
164 self.assertEqual(None, spec.approver)
165
166 def test_representation_with_no_drafter(self):
167 product = self.makeProduct()
168 name = "some-spec"
169 spec_object = self.factory.makeSpecification(
170 product=product, name=name, drafter=None)
171 # Check that the factory didn't add an drafter
172 self.assertEqual(None, spec_object.drafter)
173 spec = self.getSpecOnWebservice(spec_object)
174 # Check that it is None on the webservice too
175 self.assertEqual(None, spec.drafter)
176
177 def test_representation_with_no_assignee(self):
178 product = self.makeProduct()
179 name = "some-spec"
180 spec_object = self.factory.makeSpecification(
181 product=product, name=name, assignee=None)
182 # Check that the factory didn't add an assignee
183 self.assertEqual(None, spec_object.assignee)
184 spec = self.getSpecOnWebservice(spec_object)
185 # Check that it is None on the webservice too
186 self.assertEqual(None, spec.assignee)
187
188 def test_representation_with_no_specification_url(self):
189 product = self.makeProduct()
190 name = "some-spec"
191 spec_object = self.factory.makeSpecification(
192 product=product, name=name, specurl=None)
193 # Check that the factory didn't add an specurl
194 self.assertEqual(None, spec_object.specurl)
195 spec = self.getSpecOnWebservice(spec_object)
196 # Check that it is None on the webservice too
197 self.assertEqual(None, spec.specification_url)
198
199 def test_representation_has_project_link(self):
200 product = self.makeProduct()
201 name = "some-spec"
202 spec_object = self.factory.makeSpecification(
203 product=product, name=name)
204 spec = self.getSpecOnWebservice(spec_object)
205 self.assertEqual('fooix', spec.project.name)
206
207 def test_representation_has_project_series_link(self):
208 product = self.makeProduct()
209 productseries = self.factory.makeProductSeries(
210 name='fooix-dev', product=product)
211 name = "some-spec"
212 spec_object = self.factory.makeSpecification(
213 product=product, name=name, productseries=productseries)
214 spec = self.getSpecOnWebservice(spec_object)
215 self.assertEqual('fooix-dev', spec.project_series.name)
216
217 def test_representation_has_distribution_link(self):
218 distribution = self.makeDistribution()
219 name = "some-spec"
220 spec_object = self.factory.makeSpecification(
221 distribution=distribution, name=name)
222 spec = self.getSpecOnWebservice(spec_object)
223 self.assertEqual('foobuntu', spec.distribution.name)
224
225 def test_representation_has_distroseries_link(self):
226 distribution = self.makeDistribution()
227 distroseries = self.factory.makeDistroSeries(
228 name='maudlin', distribution=distribution)
229 name = "some-spec"
230 spec_object = self.factory.makeSpecification(
231 distribution=distribution, name=name, distroseries=distroseries)
232 spec = self.getSpecOnWebservice(spec_object)
233 self.assertEqual('maudlin', spec.distroseries.name)
234
235 def test_representation_empty_distribution(self):
236 product = self.makeProduct()
237 name = "some-spec"
238 spec_object = self.factory.makeSpecification(
239 product=product, name=name)
240 # Check that we didn't pick one up in the factory
241 self.assertEqual(None, spec_object.distribution)
242 spec = self.getSpecOnWebservice(spec_object)
243 self.assertEqual(None, spec.distribution)
244
245 def test_representation_empty_project_series(self):
246 product = self.makeProduct()
247 name = "some-spec"
248 spec_object = self.factory.makeSpecification(
249 product=product, name=name)
250 # Check that we didn't pick one up in the factory
251 self.assertEqual(None, spec_object.productseries)
252 spec = self.getSpecOnWebservice(spec_object)
253 self.assertEqual(None, spec.project_series)
254
255 def test_representation_empty_project(self):
256 distribution = self.makeDistribution()
257 name = "some-spec"
258 spec_object = self.factory.makeSpecification(
259 distribution=distribution, name=name)
260 # Check that we didn't pick one up in the factory
261 self.assertEqual(None, spec_object.product)
262 spec = self.getSpecOnWebservice(spec_object)
263 self.assertEqual(None, spec.project)
264
265 def test_representation_empty_distroseries(self):
266 distribution = self.makeDistribution()
267 name = "some-spec"
268 spec_object = self.factory.makeSpecification(
269 distribution=distribution, name=name)
270 # Check that we didn't pick one up in the factory
271 self.assertEqual(None, spec_object.distroseries)
272 spec = self.getSpecOnWebservice(spec_object)
273 self.assertEqual(None, spec.distroseries)
274
275 def test_representation_contains_milestone(self):
276 product = self.makeProduct()
277 productseries = self.factory.makeProductSeries(product=product)
278 milestone = self.factory.makeMilestone(
279 name="1.0", product=product, productseries=productseries)
280 spec_object = self.factory.makeSpecification(
281 product=product, productseries=productseries, milestone=milestone)
282 spec = self.getSpecOnWebservice(spec_object)
283 self.assertEqual("1.0", spec.milestone.name)
284
285 def test_representation_empty_milestone(self):
286 product = self.makeProduct()
287 spec_object = self.factory.makeSpecification(
288 product=product, milestone=None)
289 # Check that the factory didn't add a milestone
290 self.assertEqual(None, spec_object.milestone)
291 spec = self.getSpecOnWebservice(spec_object)
292 self.assertEqual(None, spec.milestone)
293
294
295class SpecificationTargetTests(SpecificationWebserviceTestCase):
296 """Tests for accessing specifications via their targets."""
297 layer = DatabaseFunctionalLayer
298
299 def test_get_specification_on_product(self):
300 product = self.makeProduct()
301 spec_object = self.factory.makeSpecification(
302 product=product, name="some-spec")
303 product_on_webservice = self.getPillarOnWebservice(product)
304 spec = product_on_webservice.getSpecification(name="some-spec")
305 self.assertEqual("some-spec", spec.name)
306 self.assertEqual("fooix", spec.project.name)
307
308 def test_get_specification_not_found(self):
309 product = self.makeProduct()
310 product_on_webservice = self.getPillarOnWebservice(product)
311 spec = product_on_webservice.getSpecification(name="nonexistant")
312 self.assertEqual(None, spec)
313
314 def test_get_specification_on_distribution(self):
315 distribution = self.makeDistribution()
316 spec_object = self.factory.makeSpecification(
317 distribution=distribution, name="some-spec")
318 distro_on_webservice = self.getPillarOnWebservice(distribution)
319 spec = distro_on_webservice.getSpecification(name="some-spec")
320 self.assertEqual("some-spec", spec.name)
321 self.assertEqual("foobuntu", spec.distribution.name)
322
323 def test_get_specification_on_productseries(self):
324 product = self.makeProduct()
325 productseries = self.factory.makeProductSeries(
326 product=product, name="fooix-dev")
327 spec_object = self.factory.makeSpecification(
328 product=product, name="some-spec", productseries=productseries)
329 product_on_webservice = self.getPillarOnWebservice(product)
330 productseries_on_webservice = product_on_webservice.getSeries(
331 name="fooix-dev")
332 spec = productseries_on_webservice.getSpecification(name="some-spec")
333 self.assertEqual("some-spec", spec.name)
334 self.assertEqual("fooix", spec.project.name)
335 self.assertEqual("fooix-dev", spec.project_series.name)
336
337 def test_get_specification_on_distroseries(self):
338 distribution = self.makeDistribution()
339 distroseries = self.factory.makeDistroSeries(
340 distribution=distribution, name="maudlin")
341 spec_object = self.factory.makeSpecification(
342 distribution=distribution, name="some-spec",
343 distroseries=distroseries)
344 distro_on_webservice = self.getPillarOnWebservice(distribution)
345 distroseries_on_webservice = distro_on_webservice.getSeries(
346 name_or_version="maudlin")
347 spec = distroseries_on_webservice.getSpecification(name="some-spec")
348 self.assertEqual("some-spec", spec.name)
349 self.assertEqual("foobuntu", spec.distribution.name)
350 self.assertEqual("maudlin", spec.distroseries.name)
351
352
353class IHasSpecificationsTests(SpecificationWebserviceTestCase):
354 """Tests for accessing IHasSpecifications methods over the webservice."""
355 layer = DatabaseFunctionalLayer
356
357 def assertNamesOfSpecificationsAre(self, names, specifications):
358 self.assertEqual(names, [s.name for s in specifications])
359
360 def test_product_getAllSpecifications(self):
361 product = self.makeProduct()
362 self.factory.makeSpecification(product=product, name="spec1")
363 self.factory.makeSpecification(product=product, name="spec2")
364 product_on_webservice = self.getPillarOnWebservice(product)
365 self.assertNamesOfSpecificationsAre(
366 ["spec1", "spec2"], product_on_webservice.getAllSpecifications())
367
368 def test_product_getValidSpecifications(self):
369 product = self.makeProduct()
370 self.factory.makeSpecification(product=product, name="spec1")
371 self.factory.makeSpecification(
372 product=product, name="spec2",
373 definition_status=SpecificationDefinitionStatus.OBSOLETE)
374 product_on_webservice = self.getPillarOnWebservice(product)
375 self.assertNamesOfSpecificationsAre(
376 ["spec1"], product_on_webservice.getValidSpecifications())
377
378 def test_distribution_getAllSpecifications(self):
379 distribution = self.makeDistribution()
380 self.factory.makeSpecification(
381 distribution=distribution, name="spec1")
382 self.factory.makeSpecification(
383 distribution=distribution, name="spec2")
384 distro_on_webservice = self.getPillarOnWebservice(distribution)
385 self.assertNamesOfSpecificationsAre(
386 ["spec1", "spec2"], distro_on_webservice.getAllSpecifications())
387
388 def test_distribution_getValidSpecifications(self):
389 distribution = self.makeDistribution()
390 self.factory.makeSpecification(
391 distribution=distribution, name="spec1")
392 self.factory.makeSpecification(
393 distribution=distribution, name="spec2",
394 definition_status=SpecificationDefinitionStatus.OBSOLETE)
395 distro_on_webservice = self.getPillarOnWebservice(distribution)
396 self.assertNamesOfSpecificationsAre(
397 ["spec1"], distro_on_webservice.getValidSpecifications())
398
399 def test_distroseries_getAllSpecifications(self):
400 distribution = self.makeDistribution()
401 distroseries = self.factory.makeDistroSeries(
402 name='maudlin', distribution=distribution)
403 self.factory.makeSpecification(
404 distribution=distribution, name="spec1",
405 distroseries=distroseries)
406 self.factory.makeSpecification(
407 distribution=distribution, name="spec2",
408 distroseries=distroseries)
409 self.factory.makeSpecification(
410 distribution=distribution, name="spec3")
411 distro_on_webservice = self.getPillarOnWebservice(distribution)
412 distroseries_on_webservice = distro_on_webservice.getSeries(
413 name_or_version="maudlin")
414 self.assertNamesOfSpecificationsAre(
415 ["spec1", "spec2"],
416 distroseries_on_webservice.getAllSpecifications())
417
418 def test_distroseries_getValidSpecifications(self):
419 distribution = self.makeDistribution()
420 distroseries = self.factory.makeDistroSeries(
421 name='maudlin', distribution=distribution)
422 self.factory.makeSpecification(
423 distribution=distribution, name="spec1",
424 distroseries=distroseries,
425 goalstatus=SpecificationGoalStatus.ACCEPTED)
426 self.factory.makeSpecification(
427 distribution=distribution, name="spec2",
428 goalstatus=SpecificationGoalStatus.DECLINED,
429 distroseries=distroseries)
430 self.factory.makeSpecification(
431 distribution=distribution, name="spec3",
432 distroseries=distroseries,
433 goalstatus=SpecificationGoalStatus.ACCEPTED,
434 definition_status=SpecificationDefinitionStatus.OBSOLETE)
435 self.factory.makeSpecification(
436 distribution=distribution, name="spec4")
437 distro_on_webservice = self.getPillarOnWebservice(distribution)
438 distroseries_on_webservice = distro_on_webservice.getSeries(
439 name_or_version="maudlin")
440 self.assertNamesOfSpecificationsAre(
441 ["spec1", "spec3"],
442 distroseries_on_webservice.getValidSpecifications())
443
444 def test_productseries_getAllSpecifications(self):
445 product = self.makeProduct()
446 productseries = self.factory.makeProductSeries(
447 product=product, name="fooix-dev")
448 self.factory.makeSpecification(
449 product=product, name="spec1", productseries=productseries)
450 self.factory.makeSpecification(
451 product=product, name="spec2", productseries=productseries)
452 self.factory.makeSpecification(product=product, name="spec3")
453 product_on_webservice = self.getPillarOnWebservice(product)
454 series_on_webservice = product_on_webservice.getSeries(
455 name="fooix-dev")
456 self.assertNamesOfSpecificationsAre(
457 ["spec1", "spec2"], series_on_webservice.getAllSpecifications())
458
459 def test_productseries_getValidSpecifications(self):
460 product = self.makeProduct()
461 productseries = self.factory.makeProductSeries(
462 product=product, name="fooix-dev")
463 self.factory.makeSpecification(
464 product=product, name="spec1", productseries=productseries,
465 goalstatus=SpecificationGoalStatus.ACCEPTED)
466 self.factory.makeSpecification(
467 goalstatus=SpecificationGoalStatus.DECLINED,
468 product=product, name="spec2", productseries=productseries)
469 self.factory.makeSpecification(
470 product=product, name="spec3", productseries=productseries,
471 goalstatus=SpecificationGoalStatus.ACCEPTED,
472 definition_status=SpecificationDefinitionStatus.OBSOLETE)
473 self.factory.makeSpecification(product=product, name="spec4")
474 product_on_webservice = self.getPillarOnWebservice(product)
475 series_on_webservice = product_on_webservice.getSeries(
476 name="fooix-dev")
477 # Should this be different to the results for distroseries?
478 self.assertNamesOfSpecificationsAre(
479 ["spec1", "spec2", "spec3"],
480 series_on_webservice.getAllSpecifications())
481
482 def test_projectgroup_getAllSpecifications(self):
483 productgroup = self.factory.makeProject()
484 other_productgroup = self.factory.makeProject()
485 product1 = self.factory.makeProduct(project=productgroup)
486 product2 = self.factory.makeProduct(project=productgroup)
487 product3 = self.factory.makeProduct(project=other_productgroup)
488 self.factory.makeSpecification(
489 product=product1, name="spec1")
490 self.factory.makeSpecification(
491 product=product2, name="spec2",
492 definition_status=SpecificationDefinitionStatus.OBSOLETE)
493 self.factory.makeSpecification(
494 product=product3, name="spec3")
495 product_on_webservice = self.getPillarOnWebservice(productgroup)
496 # Should this be different to the results for distroseries?
497 self.assertNamesOfSpecificationsAre(
498 ["spec1", "spec2"],
499 product_on_webservice.getAllSpecifications())
500
501 def test_projectgroup_getValidSpecifications(self):
502 productgroup = self.factory.makeProject()
503 other_productgroup = self.factory.makeProject()
504 product1 = self.factory.makeProduct(project=productgroup)
505 product2 = self.factory.makeProduct(project=productgroup)
506 product3 = self.factory.makeProduct(project=other_productgroup)
507 self.factory.makeSpecification(
508 product=product1, name="spec1")
509 self.factory.makeSpecification(
510 product=product2, name="spec2",
511 definition_status=SpecificationDefinitionStatus.OBSOLETE)
512 self.factory.makeSpecification(
513 product=product3, name="spec3")
514 product_on_webservice = self.getPillarOnWebservice(productgroup)
515 # Should this be different to the results for distroseries?
516 self.assertNamesOfSpecificationsAre(
517 ["spec1", "spec2"],
518 product_on_webservice.getValidSpecifications())
519
520 def test_person_getAllSpecifications(self):
521 person = self.factory.makePerson(name="james-w")
522 product = self.factory.makeProduct()
523 self.factory.makeSpecification(
524 product=product, name="spec1", drafter=person)
525 self.factory.makeSpecification(
526 product=product, name="spec2", approver=person,
527 definition_status=SpecificationDefinitionStatus.OBSOLETE)
528 self.factory.makeSpecification(
529 product=product, name="spec3")
530 launchpadlib = self.getLaunchpadlib()
531 person_on_webservice = launchpadlib.load(
532 str(launchpadlib._root_uri) + '/~james-w')
533 self.assertNamesOfSpecificationsAre(
534 ["spec1", "spec2"], person_on_webservice.getAllSpecifications())
535
536 def test_person_getValidSpecifications(self):
537 person = self.factory.makePerson(name="james-w")
538 product = self.factory.makeProduct()
539 self.factory.makeSpecification(
540 product=product, name="spec1", drafter=person)
541 self.factory.makeSpecification(
542 product=product, name="spec2", approver=person,
543 definition_status=SpecificationDefinitionStatus.OBSOLETE)
544 self.factory.makeSpecification(
545 product=product, name="spec3")
546 launchpadlib = self.getLaunchpadlib()
547 person_on_webservice = launchpadlib.load(
548 str(launchpadlib._root_uri) + '/~james-w')
549 self.assertNamesOfSpecificationsAre(
550 ["spec1", "spec2"], person_on_webservice.getAllSpecifications())
0551
=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py 2010-07-14 19:33:16 +0000
+++ lib/lp/testing/factory.py 2010-07-15 16:19:46 +0000
@@ -67,7 +67,8 @@
67from lp.archiveuploader.dscfile import DSCFile67from lp.archiveuploader.dscfile import DSCFile
68from lp.archiveuploader.uploadpolicy import BuildDaemonUploadPolicy68from lp.archiveuploader.uploadpolicy import BuildDaemonUploadPolicy
69from lp.blueprints.interfaces.specification import (69from lp.blueprints.interfaces.specification import (
70 ISpecificationSet, SpecificationDefinitionStatus)70 ISpecificationSet, SpecificationDefinitionStatus, SpecificationGoalStatus,
71 SpecificationPriority)
71from lp.blueprints.interfaces.sprint import ISprintSet72from lp.blueprints.interfaces.sprint import ISprintSet
7273
73from lp.bugs.interfaces.bug import CreateBugParams, IBugSet74from lp.bugs.interfaces.bug import CreateBugParams, IBugSet
@@ -1369,7 +1370,11 @@
1369 mail.parsed_string = mail.as_string()1370 mail.parsed_string = mail.as_string()
1370 return mail1371 return mail
13711372
1372 def makeSpecification(self, product=None, title=None, distribution=None):1373 def makeSpecification(self, product=None, title=None, distribution=None,
1374 name=None, specurl=None, summary=None, definition_status=None,
1375 assignee=None, drafter=None, approver=None, priority=None,
1376 owner=None, goalstatus=None, whiteboard=None, productseries=None,
1377 distroseries=None, milestone=None):
1373 """Create and return a new, arbitrary Blueprint.1378 """Create and return a new, arbitrary Blueprint.
13741379
1375 :param product: The product to make the blueprint on. If one is1380 :param product: The product to make the blueprint on. If one is
@@ -1379,15 +1384,58 @@
1379 product = self.makeProduct()1384 product = self.makeProduct()
1380 if title is None:1385 if title is None:
1381 title = self.getUniqueString('title')1386 title = self.getUniqueString('title')
1387 if name is None:
1388 name = self.getUniqueString('name')
1389 if summary is None:
1390 summary = self.getUniqueString('summary')
1391 if definition_status is None:
1392 definition_status = SpecificationDefinitionStatus.NEW
1393 if priority is None:
1394 priority = SpecificationPriority.LOW
1395 if goalstatus is None:
1396 goalstatus = SpecificationGoalStatus.PROPOSED
1397 if owner is None:
1398 owner = self.makePerson()
1399 goal_proposer = None
1400 date_goal_proposed = None
1401 if distroseries is not None or productseries is not None:
1402 goal_proposer = self.makePerson()
1403 date_goal_proposed = datetime.now(pytz.UTC)
1404 goal_decider = None
1405 date_goal_decided = None
1406 if goalstatus != SpecificationGoalStatus.PROPOSED:
1407 goal_decider = self.makePerson()
1408 date_goal_decided = datetime.now(pytz.UTC)
1409 completer = None
1410 date_completed = None
1411 if definition_status == SpecificationDefinitionStatus.OBSOLETE:
1412 completer = self.makePerson()
1413 date_completed = datetime.now(pytz.UTC)
1382 return getUtility(ISpecificationSet).new(1414 return getUtility(ISpecificationSet).new(
1383 name=self.getUniqueString('name'),1415 name=name,
1384 title=title,1416 title=title,
1385 specurl=None,1417 specurl=specurl,
1386 summary=self.getUniqueString('summary'),1418 summary=summary,
1387 definition_status=SpecificationDefinitionStatus.NEW,1419 definition_status=definition_status,
1388 owner=self.makePerson(),1420 owner=owner,
1389 product=product,1421 product=product,
1390 distribution=distribution)1422 productseries=productseries,
1423 distribution=distribution,
1424 distroseries=distroseries,
1425 assignee=assignee,
1426 drafter=drafter,
1427 approver=approver,
1428 priority=priority,
1429 goalstatus=goalstatus,
1430 whiteboard=whiteboard,
1431 goal_proposer=goal_proposer,
1432 date_goal_proposed=date_goal_proposed,
1433 milestone=milestone,
1434 date_completed=date_completed,
1435 completer=completer,
1436 goal_decider=goal_decider,
1437 date_goal_decided=date_goal_decided,
1438 )
13911439
1392 def makeQuestion(self, target=None, title=None):1440 def makeQuestion(self, target=None, title=None):
1393 """Create and return a new, arbitrary Question.1441 """Create and return a new, arbitrary Question.