Merge lp:~abentley/launchpad/build-from-recipe-api into lp:launchpad

Proposed by Aaron Bentley
Status: Merged
Merged at revision: not available
Proposed branch: lp:~abentley/launchpad/build-from-recipe-api
Merge into: lp:launchpad
Diff against target: 786 lines (+257/-52)
21 files modified
lib/canonical/launchpad/components/apihelpers.py (+15/-0)
lib/canonical/launchpad/doc/tales.txt (+3/-3)
lib/canonical/launchpad/interfaces/_schema_circular_imports.py (+13/-2)
lib/lp/code/browser/tests/test_sourcepackagerecipe.py (+1/-1)
lib/lp/code/interfaces/sourcepackagerecipe.py (+42/-11)
lib/lp/code/interfaces/sourcepackagerecipebuild.py (+2/-0)
lib/lp/code/interfaces/webservice.py (+2/-0)
lib/lp/code/model/sourcepackagerecipe.py (+12/-0)
lib/lp/code/tests/test_sourcepackagerecipe.py (+73/-3)
lib/lp/registry/interfaces/person.py (+26/-1)
lib/lp/registry/model/person.py (+14/-0)
lib/lp/soyuz/browser/tests/archivesubscription-views.txt (+13/-13)
lib/lp/soyuz/doc/archive.txt (+1/-1)
lib/lp/soyuz/doc/archiveauthtoken.txt (+1/-1)
lib/lp/soyuz/doc/sourcepackagerelease.txt (+4/-4)
lib/lp/soyuz/model/archive.py (+1/-2)
lib/lp/soyuz/scripts/tests/test_ppa_add_missing_builds.py (+1/-0)
lib/lp/soyuz/stories/ppa/xx-ppa-packages.txt (+1/-1)
lib/lp/testing/__init__.py (+17/-1)
lib/lp/testing/_webservice.py (+3/-0)
lib/lp/testing/factory.py (+12/-8)
To merge this branch: bzr merge lp:~abentley/launchpad/build-from-recipe-api
Reviewer Review Type Date Requested Status
Tim Penhey (community) Approve
Review via email: mp+23981@code.launchpad.net

Commit message

Expose SourcePackageRecipe over the web service

Description of the change

= Summary =
Provide SourcePackageRecipe over the web service

== Proposed fix ==
See above

== Pre-implementation notes ==
Discussion with the entire code team, at various points.

== Implementation details ==
Added Person.createRecipe, since we can't expose SourcePackageRecipeSource.new.
Exposed SourcePackageRecipe, created setRecipeText, which will automatically
parse and store the recipe. Exposed SourcePackageRecipe.requestBuild.

SourcePackageRecipe.distroseries is a ReferenceSet, (not a ResultSet), and
these cannot be exposed yet. Further work is required to either change its
type or make its type exposable.

LaunchpadObjectFactory.makeArchive creates PPAs by default, which is useful,
but they are non-Ubuntu PPAs, and as such are not accessible via the web
service. This branch changes makeArchive to use Ubuntu when creating ppas
(unless overridden).

Implemented patch_list_parameter_type because it had not yet been invented.

Implemented ws_obj to make it convenient to translate a database object into a webservice object.

== Tests ==
bin/test -t TestWebservice test_sourcepackagerecipe

== Demo and Q/A ==
None

= Launchpad lint =

Checking for conflicts. and issues in doctests and templates.
Running jslint, xmllint, pyflakes, and pylint.
Using normal rules.

Linting changed files:
  lib/lp/code/interfaces/sourcepackagerecipebuild.py
  lib/lp/code/interfaces/webservice.py
  lib/canonical/launchpad/components/apihelpers.py
  lib/lp/code/tests/test_sourcepackagerecipe.py
  lib/lp/testing/factory.py
  lib/lp/testing/__init__.py
  lib/lp/testing/_webservice.py
  lib/lp/registry/interfaces/person.py
  lib/lp/code/model/sourcepackagerecipe.py
  lib/lp/code/interfaces/sourcepackagerecipe.py
  lib/lp/registry/model/person.py
  lib/canonical/launchpad/interfaces/_schema_circular_imports.py

== Pyflakes notices ==

lib/lp/code/interfaces/webservice.py
    8: 'CodeImportAlreadyRunning' imported but unused
    8: 'CodeImportNotInReviewedState' imported but unused
    8: 'BranchMergeProposalExists' imported but unused
    11: 'BranchCreatorNotOwner' imported but unused
    11: 'IBranchSet' imported but unused
    11: 'IBranch' imported but unused
    11: 'BranchExists' imported but unused
    11: 'BranchCreatorNotMemberOfOwnerTeam' imported but unused
    14: 'IBranchMergeProposal' imported but unused
    15: 'IBranchSubscription' imported but unused
    16: 'ICodeImport' imported but unused
    17: 'ICodeReviewComment' imported but unused
    18: 'ICodeReviewVoteReference' imported but unused
    19: 'IStaticDiff' imported but unused
    19: 'IDiff' imported but unused
    19: 'IPreviewDiff' imported but unused
    20: 'ISourcePackageRecipe' imported but unused
    21: 'ISourcePackageRecipeBuild' imported but unused

== Pylint notices ==

lib/lp/code/interfaces/webservice.py
    21: [C0301] Line too long (81/78)
    19: [W0611] Unused import IDiff
    8: [W0611] Unused import CodeImportAlreadyRunning
    15: [W0611] Unused import IBranchSubscription
    11: [W0611] Unused import BranchCreatorNotOwner
    8: [W0611] Unused import CodeImportNotInReviewedState
    14: [W0611] Unused import IBranchMergeProposal
    11: [W0611] Unused import BranchCreatorNotMemberOfOwnerTeam
    16: [W0611] Unused import ICodeImport
    20: [W0611] Unused import ISourcePackageRecipe
    18: [W0611] Unused import ICodeReviewVoteReference
    11: [W0611] Unused import IBranchSet
    19: [W0611] Unused import IStaticDiff
    11: [W0611] Unused import IBranch
    19: [W0611] Unused import IPreviewDiff
    8: [W0611] Unused import BranchMergeProposalExists
    11: [W0611] Unused import BranchExists
    21: [W0611] Unused import ISourcePackageRecipeBuild
    17: [W0611] Unused import ICodeReviewComment

lib/lp/code/tests/test_sourcepackagerecipe.py
    245: [E1002, TestRecipeBranchRoundTripping.setUp] Use super on an old style class

lib/lp/registry/interfaces/person.py
    411: [E1002, PersonNameField._validate] Use super on an old style class
    851: [C0322, IPersonPublic.createRecipe] Operator not preceded by a space
    distroseries=List(value_type=Reference(schema=Interface)),
    ^
    name=TextLine(),
    recipe_text=Text(),
    sourcepackagename=TextLine(),
    )
    @export_factory_operation(Interface, [])
    def createRecipe(name, description, recipe_text, distroseries,
    sourcepackagename, registrant):
    1408: [C0322, IPersonEditRestricted.addMember] Operator not preceded by a space
    status=copy_field(ITeamMembership['status']),
    ^
    comment=Text(required=False))
    @export_write_operation()
    def addMember(person, reviewer, status=TeamMembershipStatus.APPROVED,
    comment=None, force_team_add=False,
    may_subscribe_to_list=True):
    1449: [C0322, IPersonEditRestricted.acceptInvitationToBeMemberOf] Operator not preceded by a space
    comment=Text())
    ^
    @export_write_operation()
    def acceptInvitationToBeMemberOf(team, comment):
    1461: [C0322, IPersonEditRestricted.declineInvitationToBeMemberOf] Operator not preceded by a space
    comment=Text())
    ^
    @export_write_operation()
    def declineInvitationToBeMemberOf(team, comment):
    1749: [C0322, IPersonSet.newTeam] Operator not preceded by a space
    defaultmembershipperiod='default_membership_period',
    ^
    defaultrenewalperiod='default_renewal_period')
    @operation_parameters(
    subscriptionpolicy=Choice(
    title=_('Subscription policy'), vocabulary=TeamSubscriptionPolicy,
    required=False, default=TeamSubscriptionPolicy.MODERATED))
    @export_factory_operation(
    ITeam, ['name', 'displayname', 'teamdescription',
    'defaultmembershipperiod', 'defaultrenewalperiod'])
    def newTeam(teamowner, name, displayname, teamdescription=None,
    subscriptionpolicy=TeamSubscriptionPolicy.MODERATED,
    defaultmembershipperiod=None, defaultrenewalperiod=None):
    1818: [C0322, IPersonSet.findPerson] Operator not preceded by a space
    created_after=Datetime(
    ^
    title=_("Created after"), required=False),
    created_before=Datetime(
    title=_("Created before"), required=False),
    )
    @operation_returns_collection_of(IPerson)
    @export_read_operation()
    def findPerson(text="", exclude_inactive_accounts=True,
    must_have_email=False,
    created_after=None, created_before=None):

lib/lp/code/interfaces/sourcepackagerecipe.py
    132: [C0322, ISourcePackageRecipe.requestBuild] Operator not preceded by a space
    distroseries=Reference(schema=IDistroSeries),
    ^
    )
    @export_write_operation()
    def requestBuild(archive, distroseries, requester, pocket):

lib/lp/registry/model/person.py
    301: [W1001, Person] Use of "property" on an old style class
    317: [W1001, Person] Use of "property" on an old style class
    331: [W1001, Person] Use of "property" on an old style class
    1263: [W0104, Person.addMember] Statement seems to have no effect

To post a comment you must log in.
Revision history for this message
Tim Penhey (thumper) wrote :

The exported registrant and owner in the interface need to be slightly
more constrained. Looking at the IBranch interface, it seems that
'PublicPersonChoice' is the better choice for the registrant rather than just
Reference. I recall there being a big hoo-ha over it when we initially had
some privacy issues around person.

    registrant = exported(
        PublicPersonChoice(
            title=_("The person who created this recipe."),
            required=True, readonly=True,
            vocabulary='ValidPersonOrTeam'))

And similarly, the owner should be constrained to the person or a team the
person is a member of, otherwise someone would be able to pass off the recipe
to anyone else (which we don't support elsewhere).

    owner = exported(
        ParticipatingPersonChoice(
            title=_('Owner'),
            required=True, readonly=False,
            vocabulary='UserTeamsParticipationPlusSelf',
            description=_("The person or team who can edit this recipe.")))

In the operation_parameters for requestBuild you have pocket one space
dedented from the others.

lib/lp/registry/interfaces/person.py
 - You need a blank line between the end of createRecipe and getRecipe.

review: Needs Fixing
Revision history for this message
Aaron Bentley (abentley) wrote :

Updated per your review.

Revision history for this message
Tim Penhey (thumper) wrote :

Apart from the merge conflicts, this looks good now.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/canonical/launchpad/components/apihelpers.py'
2--- lib/canonical/launchpad/components/apihelpers.py 2009-06-25 05:30:52 +0000
3+++ lib/canonical/launchpad/components/apihelpers.py 2010-04-29 19:44:31 +0000
4@@ -19,6 +19,7 @@
5 'patch_collection_property',
6 'patch_collection_return_type',
7 'patch_plain_parameter_type',
8+ 'patch_list_parameter_type',
9 'patch_reference_property',
10 ]
11
12@@ -48,6 +49,20 @@
13 collection['return_type'].value_type.schema = return_type
14
15
16+def patch_list_parameter_type(exported_class, method_name, param_name,
17+ param_type):
18+ """Update a list parameter type for a webservice method.
19+
20+ :param exported_class: The class containing the method.
21+ :param method_name: The method name that you need to patch.
22+ :param param_name: The name of the parameter that you need to patch.
23+ :param param_type: The new type for the parameter.
24+ """
25+ method = exported_class[method_name]
26+ params = method.queryTaggedValue(LAZR_WEBSERVICE_EXPORTED)['params']
27+ params[param_name].value_type = param_type
28+
29+
30 def patch_plain_parameter_type(exported_class, method_name, param_name,
31 param_type):
32 """Update a plain parameter type for a webservice method.
33
34=== modified file 'lib/canonical/launchpad/doc/tales.txt'
35--- lib/canonical/launchpad/doc/tales.txt 2010-04-16 15:06:55 +0000
36+++ lib/canonical/launchpad/doc/tales.txt 2010-04-29 19:44:31 +0000
37@@ -157,10 +157,10 @@
38
39 >>> login('admin@canonical.com')
40 >>> private_ppa = factory.makeArchive(
41- ... name='ppa', private=True, owner=owner)
42+ ... name='pppa', private=True, owner=owner)
43
44 >>> print test_tales("ppa/fmt:link", ppa=private_ppa)
45- <a href="/~joe/+archive/ppa" class="sprite ppa-icon">PPA
46+ <a href="/~joe/+archive/pppa" class="sprite ppa-icon">PPA named pppa
47 for Joe Smith</a>
48
49 >>> login(ANONYMOUS)
50@@ -178,7 +178,7 @@
51 >>> ignore = private_ppa.newSubscription(ppa_user, owner)
52 >>> login_person(ppa_user)
53 >>> print test_tales("ppa/fmt:reference", ppa=private_ppa)
54- ppa:joe/ppa
55+ ppa:joe/pppa
56
57 We also have icons for builds which may have different dimensions.
58
59
60=== modified file 'lib/canonical/launchpad/interfaces/_schema_circular_imports.py'
61--- lib/canonical/launchpad/interfaces/_schema_circular_imports.py 2010-04-13 15:12:18 +0000
62+++ lib/canonical/launchpad/interfaces/_schema_circular_imports.py 2010-04-29 19:44:31 +0000
63@@ -16,11 +16,13 @@
64
65
66 from lazr.restful.declarations import LAZR_WEBSERVICE_EXPORTED
67+from lazr.restful.fields import Reference
68
69 from canonical.launchpad.components.apihelpers import (
70 patch_entry_return_type, patch_collection_property,
71- patch_collection_return_type, patch_plain_parameter_type,
72- patch_choice_parameter_type, patch_reference_property)
73+ patch_collection_return_type, patch_list_parameter_type,
74+ patch_plain_parameter_type, patch_choice_parameter_type,
75+ patch_reference_property)
76
77 from lp.registry.interfaces.structuralsubscription import (
78 IStructuralSubscription, IStructuralSubscriptionTarget)
79@@ -46,6 +48,8 @@
80 from lp.code.interfaces.diff import IPreviewDiff
81 from lp.code.interfaces.hasbranches import (
82 IHasBranches, IHasCodeImports, IHasMergeProposals, IHasRequestedReviews)
83+from lp.code.interfaces.sourcepackagerecipe import (
84+ ISourcePackageRecipe)
85 from lp.code.interfaces.sourcepackagerecipebuild import (
86 ISourcePackageRecipeBuild)
87 from lp.registry.interfaces.distribution import IDistribution
88@@ -185,6 +189,13 @@
89 LAZR_WEBSERVICE_EXPORTED)['params']['branch'].schema = IBranch
90 patch_reference_property(ISourcePackage, 'distribution', IDistribution)
91
92+# IPerson
93+patch_entry_return_type(IPerson, 'createRecipe', ISourcePackageRecipe)
94+patch_list_parameter_type(IPerson, 'createRecipe', 'distroseries',
95+ Reference(schema=IDistroSeries))
96+
97+patch_entry_return_type(IPerson, 'getRecipe', ISourcePackageRecipe)
98+
99 IPerson['hardware_submissions'].value_type.schema = IHWSubmission
100
101 # publishing.py
102
103=== modified file 'lib/lp/code/browser/tests/test_sourcepackagerecipe.py'
104--- lib/lp/code/browser/tests/test_sourcepackagerecipe.py 2010-04-28 16:01:10 +0000
105+++ lib/lp/code/browser/tests/test_sourcepackagerecipe.py 2010-04-29 19:44:31 +0000
106@@ -35,7 +35,7 @@
107 self.chef = self.factory.makePerson(
108 displayname='Master Chef', name='chef', password='test')
109 self.ppa = self.factory.makeArchive(
110- displayname='Secret PPA', owner=self.chef)
111+ displayname='Secret PPA', owner=self.chef, name='ppa')
112 self.squirrel = self.factory.makeDistroSeries(
113 displayname='Secret Squirrel', name='secret')
114
115
116=== modified file 'lib/lp/code/interfaces/sourcepackagerecipe.py'
117--- lib/lp/code/interfaces/sourcepackagerecipe.py 2010-04-21 01:59:57 +0000
118+++ lib/lp/code/interfaces/sourcepackagerecipe.py 2010-04-29 19:44:31 +0000
119@@ -21,19 +21,25 @@
120
121 from textwrap import dedent
122
123+from lazr.restful.declarations import (
124+ call_with, export_as_webservice_entry, export_write_operation, exported,
125+ operation_parameters, REQUEST_USER)
126 from lazr.restful.fields import CollectionField, Reference
127 from zope.interface import Attribute, Interface
128-from zope.schema import Bool, Datetime, Object, Text, TextLine
129+from zope.schema import Bool, Choice, Datetime, Object, Text, TextLine
130
131 from canonical.launchpad import _
132-from canonical.launchpad.fields import ParticipatingPersonChoice
133+from canonical.launchpad.fields import (
134+ ParticipatingPersonChoice, PublicPersonChoice
135+)
136 from canonical.launchpad.validators.name import name_validator
137
138 from lp.code.interfaces.branch import IBranch
139-from lp.registry.interfaces.person import IPerson
140+from lp.registry.interfaces.pocket import PackagePublishingPocket
141 from lp.registry.interfaces.role import IHasOwner
142 from lp.registry.interfaces.distroseries import IDistroSeries
143 from lp.registry.interfaces.sourcepackagename import ISourcePackageName
144+from lp.soyuz.interfaces.archive import IArchive
145
146
147 MINIMAL_RECIPE_TEXT = dedent(u'''\
148@@ -80,16 +86,24 @@
149 More precisely, it describes how to combine a number of branches into a
150 debianized source tree.
151 """
152+ export_as_webservice_entry()
153
154 date_created = Datetime(required=True, readonly=True)
155 date_last_modified = Datetime(required=True, readonly=True)
156
157- registrant = Reference(
158- IPerson, title=_("The person who created this recipe"), readonly=True)
159- owner = ParticipatingPersonChoice(
160- title=_('Owner'), required=True, readonly=False,
161- vocabulary='UserTeamsParticipationPlusSelf',
162- description=_("The person or team who can edit this recipe."))
163+ registrant = exported(
164+ PublicPersonChoice(
165+ title=_("The person who created this recipe."),
166+ required=True, readonly=True,
167+ vocabulary='ValidPersonOrTeam'))
168+
169+ owner = exported(
170+ ParticipatingPersonChoice(
171+ title=_('Owner'),
172+ required=True, readonly=False,
173+ vocabulary='UserTeamsParticipationPlusSelf',
174+ description=_("The person or team who can edit this recipe.")))
175+
176 distroseries = CollectionField(
177 Reference(IDistroSeries), title=_("The distroseries this recipe will"
178 " build a source package for"),
179@@ -101,10 +115,13 @@
180 "recipe will build a source package"),
181 readonly=True)
182
183- name = TextLine(
184+ _sourcepackagename_text = exported(
185+ TextLine(), exported_as='sourcepackagename')
186+
187+ name = exported(TextLine(
188 title=_("Name"), required=True,
189 constraint=name_validator,
190- description=_("The name of this recipe."))
191+ description=_("The name of this recipe.")))
192
193 description = Text(
194 title=_('Description'), required=True,
195@@ -117,6 +134,20 @@
196 IBranch, title=_("The base branch used by this recipe."),
197 required=True, readonly=True)
198
199+ @operation_parameters(recipe_text=Text())
200+ @export_write_operation()
201+ def setRecipeText(recipe_text):
202+ """Set the text of the recipe."""
203+
204+ recipe_text = exported(Text())
205+
206+ @call_with(requester=REQUEST_USER)
207+ @operation_parameters(
208+ archive=Reference(schema=IArchive),
209+ distroseries=Reference(schema=IDistroSeries),
210+ pocket=Choice(vocabulary=PackagePublishingPocket,)
211+ )
212+ @export_write_operation()
213 def requestBuild(archive, distroseries, requester, pocket):
214 """Request that the recipe be built in to the specified archive.
215
216
217=== modified file 'lib/lp/code/interfaces/sourcepackagerecipebuild.py'
218--- lib/lp/code/interfaces/sourcepackagerecipebuild.py 2010-03-16 05:08:47 +0000
219+++ lib/lp/code/interfaces/sourcepackagerecipebuild.py 2010-04-29 19:44:31 +0000
220@@ -14,6 +14,7 @@
221 ]
222
223 from lazr.restful.fields import Reference
224+from lazr.restful.declarations import export_as_webservice_entry
225
226 from zope.interface import Interface
227 from zope.schema import Int, Object
228@@ -32,6 +33,7 @@
229
230 class ISourcePackageRecipeBuild(IBuildBase):
231 """A build of a source package."""
232+ export_as_webservice_entry()
233
234 id = Int(title=_("Identifier for this build."))
235
236
237=== modified file 'lib/lp/code/interfaces/webservice.py'
238--- lib/lp/code/interfaces/webservice.py 2010-04-02 20:31:43 +0000
239+++ lib/lp/code/interfaces/webservice.py 2010-04-29 19:44:31 +0000
240@@ -17,3 +17,5 @@
241 from lp.code.interfaces.codereviewcomment import ICodeReviewComment
242 from lp.code.interfaces.codereviewvote import ICodeReviewVoteReference
243 from lp.code.interfaces.diff import IDiff, IPreviewDiff, IStaticDiff
244+from lp.code.interfaces.sourcepackagerecipe import ISourcePackageRecipe
245+from lp.code.interfaces.sourcepackagerecipebuild import ISourcePackageRecipeBuild
246
247=== modified file 'lib/lp/code/model/sourcepackagerecipe.py'
248--- lib/lp/code/model/sourcepackagerecipe.py 2010-04-19 05:04:00 +0000
249+++ lib/lp/code/model/sourcepackagerecipe.py 2010-04-29 19:44:31 +0000
250@@ -10,6 +10,7 @@
251 'SourcePackageRecipe',
252 ]
253
254+from bzrlib.plugins.builder import RecipeParser
255 from lazr.delegates import delegates
256
257 from storm.locals import (
258@@ -80,6 +81,10 @@
259 sourcepackagename = Reference(
260 sourcepackagename_id, 'SourcePackageName.id')
261
262+ @property
263+ def _sourcepackagename_text(self):
264+ return self.sourcepackagename.name
265+
266 name = Unicode(allow_none=True)
267 description = Unicode(allow_none=False)
268
269@@ -103,6 +108,13 @@
270 def base_branch(self):
271 return self._recipe_data.base_branch
272
273+ def setRecipeText(self, recipe_text):
274+ self.builder_recipe = RecipeParser(recipe_text).parse()
275+
276+ @property
277+ def recipe_text(self):
278+ return str(self.builder_recipe)
279+
280 @staticmethod
281 def new(registrant, owner, distroseries, sourcepackagename, name,
282 builder_recipe, description):
283
284=== modified file 'lib/lp/code/tests/test_sourcepackagerecipe.py'
285--- lib/lp/code/tests/test_sourcepackagerecipe.py 2010-03-26 19:29:56 +0000
286+++ lib/lp/code/tests/test_sourcepackagerecipe.py 2010-04-29 19:44:31 +0000
287@@ -12,11 +12,12 @@
288
289 from storm.locals import Store
290
291+import transaction
292 from zope.component import getUtility
293 from zope.security.interfaces import Unauthorized
294 from zope.security.proxy import removeSecurityProxy
295
296-from canonical.testing.layers import DatabaseFunctionalLayer
297+from canonical.testing.layers import DatabaseFunctionalLayer, AppServerLayer
298
299 from lp.archiveuploader.permission import (
300 ArchiveDisabled, CannotUploadToArchive, InvalidPocketForPPA)
301@@ -24,7 +25,7 @@
302 from lp.buildmaster.model.buildqueue import BuildQueue
303 from lp.code.interfaces.sourcepackagerecipe import (
304 ForbiddenInstruction, ISourcePackageRecipe, ISourcePackageRecipeSource,
305- TooNewRecipeFormat)
306+ TooNewRecipeFormat, MINIMAL_RECIPE_TEXT)
307 from lp.code.interfaces.sourcepackagerecipebuild import (
308 ISourcePackageRecipeBuild, ISourcePackageRecipeBuildJob)
309 from lp.code.model.sourcepackagerecipebuild import (
310@@ -35,7 +36,9 @@
311 from lp.services.job.interfaces.job import (
312 IJob, JobStatus)
313 from lp.soyuz.interfaces.archive import ArchivePurpose
314-from lp.testing import login_person, TestCaseWithFactory
315+from lp.testing import (
316+ ANONYMOUS, launchpadlib_for, login, login_person, TestCaseWithFactory,
317+ ws_object)
318
319
320 class TestSourcePackageRecipe(TestCaseWithFactory):
321@@ -425,5 +428,72 @@
322 child_branch, "zam", self.merged_branch.bzr_identity, revspec="2")
323
324
325+class TestWebservice(TestCaseWithFactory):
326+
327+ layer = AppServerLayer
328+
329+ def makeRecipeText(self):
330+ branch = self.factory.makeBranch()
331+ return MINIMAL_RECIPE_TEXT % branch.bzr_identity
332+
333+ def makeRecipe(self, user=None, owner=None, recipe_text=None):
334+ if user is None:
335+ user = self.factory.makePerson()
336+ if owner is None:
337+ owner = user
338+ db_distroseries = self.factory.makeDistroSeries()
339+ if recipe_text is None:
340+ recipe_text = self.makeRecipeText()
341+ launchpad = launchpadlib_for('test', user,
342+ service_root="http://api.launchpad.dev:8085")
343+ login(ANONYMOUS)
344+ distroseries = ws_object(launchpad, db_distroseries)
345+ ws_owner = ws_object(launchpad, owner)
346+ recipe = ws_owner.createRecipe(
347+ name='toaster-1', sourcepackagename='toaster',
348+ description='a recipe', distroseries=[distroseries.self_link],
349+ recipe_text=recipe_text)
350+ # at the moment, distroseries is not exposed in the API.
351+ transaction.commit()
352+ db_recipe = owner.getRecipe(name=u'toaster-1')
353+ self.assertEqual(set([db_distroseries]), set(db_recipe.distroseries))
354+ return recipe, ws_owner, launchpad
355+
356+ def test_createRecipe(self):
357+ """Ensure recipe creation works."""
358+ team = self.factory.makeTeam()
359+ recipe_text = self.makeRecipeText()
360+ recipe, user = self.makeRecipe(user=team.teamowner, owner=team,
361+ recipe_text=recipe_text)[:2]
362+ self.assertEqual(team.name, recipe.owner.name)
363+ self.assertEqual(team.teamowner.name, recipe.registrant.name)
364+ self.assertEqual('toaster-1', recipe.name)
365+ self.assertEqual(recipe_text, recipe.recipe_text)
366+ self.assertEqual('toaster', recipe.sourcepackagename)
367+
368+ def test_recipe_text(self):
369+ recipe_text2 = self.makeRecipeText()
370+ recipe = self.makeRecipe()[0]
371+ recipe.setRecipeText(recipe_text=recipe_text2)
372+ self.assertEqual(recipe_text2, recipe.recipe_text)
373+
374+ def test_getRecipe(self):
375+ """Person.getRecipe returns the named recipe."""
376+ recipe, user = self.makeRecipe()[:-1]
377+ self.assertEqual(recipe, user.getRecipe(name=recipe.name))
378+
379+ def test_requestBuild(self):
380+ """Build requests can be performed."""
381+ person = self.factory.makePerson()
382+ archive = self.factory.makeArchive(owner=person)
383+ distroseries = self.factory.makeDistroSeries()
384+ recipe, user, launchpad = self.makeRecipe(person)
385+ distroseries = ws_object(launchpad, distroseries)
386+ archive = ws_object(launchpad, archive)
387+ recipe.requestBuild(
388+ archive=archive, distroseries=distroseries,
389+ pocket=PackagePublishingPocket.RELEASE.title)
390+
391+
392 def test_suite():
393 return unittest.TestLoader().loadTestsFromName(__name__)
394
395=== modified file 'lib/lp/registry/interfaces/person.py'
396--- lib/lp/registry/interfaces/person.py 2010-04-27 11:48:16 +0000
397+++ lib/lp/registry/interfaces/person.py 2010-04-29 19:44:31 +0000
398@@ -41,7 +41,8 @@
399
400
401 from zope.formlib.form import NoInputData
402-from zope.schema import Bool, Choice, Datetime, Int, Object, Text, TextLine
403+from zope.schema import (
404+ Bool, Choice, Datetime, Int, List, Object, Text, TextLine)
405 from zope.interface import Attribute, Interface
406 from zope.interface.exceptions import Invalid
407 from zope.interface.interface import invariant
408@@ -844,6 +845,30 @@
409 not teams can be converted into teams.
410 """
411
412+ @call_with(registrant=REQUEST_USER)
413+ @operation_parameters(
414+ description=Text(),
415+ distroseries=List(value_type=Reference(schema=Interface)),
416+ name=TextLine(),
417+ recipe_text=Text(),
418+ sourcepackagename=TextLine(),
419+ )
420+ @export_factory_operation(Interface, [])
421+ def createRecipe(name, description, recipe_text, distroseries,
422+ sourcepackagename, registrant):
423+ """Create a SourcePackageRecipe owned by this person.
424+
425+ :param name: the name to use for referring to the recipe.
426+ :param description: A description of the recipe.
427+ :param recipe_text: The text of the recipe.
428+ :param distroseries: The distroseries to use.
429+ :param sourcepackagename: The name of the sourcepackage for the recipe.
430+ :return: a SourcePackageRecipe.
431+ """
432+
433+ @operation_parameters(name=TextLine(required=True))
434+ @operation_returns_entry(Interface) # Really ISourcePackageRecipe.
435+ @export_read_operation()
436 def getRecipe(name):
437 """Return the person's recipe with the given name."""
438
439
440=== modified file 'lib/lp/registry/model/person.py'
441--- lib/lp/registry/model/person.py 2010-04-26 15:59:41 +0000
442+++ lib/lp/registry/model/person.py 2010-04-29 19:44:31 +0000
443@@ -32,6 +32,7 @@
444 import re
445 import weakref
446
447+from bzrlib.plugins.builder import RecipeParser
448 from zope.lifecycleevent import ObjectCreatedEvent
449 from zope.interface import alsoProvides, implementer, implements
450 from zope.component import adapter, getUtility
451@@ -120,6 +121,8 @@
452 SpecificationDefinitionStatus, SpecificationFilter,
453 SpecificationImplementationStatus, SpecificationSort)
454 from canonical.launchpad.interfaces.lpstorm import IStore
455+from lp.registry.interfaces.sourcepackagename import (
456+ ISourcePackageNameSet)
457 from lp.registry.interfaces.ssh import ISSHKey, ISSHKeySet, SSHKeyType
458 from lp.registry.interfaces.teammembership import (
459 TeamMembershipStatus)
460@@ -2265,6 +2268,17 @@
461
462 return rset
463
464+ def createRecipe(self, name, description, recipe_text, distroseries,
465+ sourcepackagename, registrant):
466+ """See `IPerson`."""
467+ from lp.code.model.sourcepackagerecipe import SourcePackageRecipe
468+ builder_recipe = RecipeParser(recipe_text).parse()
469+ spnset = getUtility(ISourcePackageNameSet)
470+ sourcepackagename = spnset.getOrCreateByName(sourcepackagename)
471+ return SourcePackageRecipe.new(
472+ registrant, self, distroseries, sourcepackagename, name,
473+ builder_recipe, description)
474+
475 def getRecipe(self, name):
476 from lp.code.model.sourcepackagerecipe import SourcePackageRecipe
477 return Store.of(self).find(
478
479=== modified file 'lib/lp/soyuz/browser/tests/archivesubscription-views.txt'
480--- lib/lp/soyuz/browser/tests/archivesubscription-views.txt 2010-02-23 15:10:46 +0000
481+++ lib/lp/soyuz/browser/tests/archivesubscription-views.txt 2010-04-29 19:44:31 +0000
482@@ -23,10 +23,10 @@
483 >>> from lp.registry.interfaces.person import IPersonSet
484 >>> cprov = getUtility(IPersonSet).getByName("cprov")
485 >>> cprov_private_ppa = factory.makeArchive(
486- ... owner=cprov, private=True)
487+ ... owner=cprov, private=True, name='pppa')
488 >>> mark = getUtility(IPersonSet).getByName("mark")
489 >>> mark_private_ppa = factory.makeArchive(
490- ... owner=mark, private=True)
491+ ... owner=mark, private=True, name='pppa')
492 >>> transaction.commit()
493 >>> logout()
494
495@@ -36,7 +36,7 @@
496 >>> view = create_initialized_view(
497 ... cprov_private_ppa, name="+subscriptions")
498 >>> print view.label
499- Manage access to PPA for Celso Providelo
500+ Manage access to PPA named pppa for Celso Providelo
501
502 Initially the view does not display any subscribers, as can be seen
503 using the has_subscriptions property:
504@@ -86,7 +86,7 @@
505 >>> for notification in view.request.notifications:
506 ... print notification.message
507 You have granted access for Andrew Bennetts to install software
508- from PPA for Celso Providelo.
509+ from PPA named pppa for Celso Providelo.
510 Andrew Bennetts will be notified of the access via email.
511
512 The view includes a subscribers property that returns all the current
513@@ -182,7 +182,7 @@
514 ... cprov_private_ppa).one()
515 >>> view = create_initialized_view(spiv_subscription, name="+edit")
516 >>> print view.label
517- Edit Andrew Bennetts's access to PPA for Celso Providelo
518+ Edit Andrew Bennetts's access to PPA named pppa for Celso Providelo
519
520 The ArchiveSubscriptionEditView presents the expiry and description ready
521 for editing, together with Update and Cancel actions:
522@@ -197,7 +197,7 @@
523 The ArchiveSubscriptionEditView has a next_url helper property.
524
525 >>> print view.next_url
526- http://launchpad.dev/~cprov/+archive/ppa/+subscriptions
527+ http://launchpad.dev/~cprov/+archive/pppa/+subscriptions
528
529 The ArchiveSubscriptionEditView can be used to update the description field:
530
531@@ -253,7 +253,7 @@
532
533 >>> for notification in view.request.notifications:
534 ... print notification.message
535- You have revoked Andrew Bennetts's access to PPA for
536+ You have revoked Andrew Bennetts's access to PPA named pppa for
537 Celso Providelo.
538
539 Just uncancel the subscription before continuing on.
540@@ -295,8 +295,8 @@
541 ... print token_text
542
543 >>> print_subscriptions_with_tokens()
544- PPA for Mark Shuttleworth None
545- PPA for Celso Providelo None
546+ PPA named pppa for Mark Shuttleworth None
547+ PPA named pppa for Celso Providelo None
548
549 After activating a subscription, the token will be included in the
550 subscriptions_with_tokens property:
551@@ -305,8 +305,8 @@
552 >>> new_token = cprov_private_ppa.newAuthToken(spiv)
553 >>> view = create_initialized_view(spiv, name="+archivesubscriptions")
554 >>> print_subscriptions_with_tokens()
555- PPA for Mark Shuttleworth None
556- PPA for Celso Providelo Token
557+ PPA named pppa for Mark Shuttleworth None
558+ PPA named pppa for Celso Providelo Token
559
560 Just deactivate the new token again for the remaining tests.
561
562@@ -326,7 +326,7 @@
563 ... spiv_subscription.subscriber, spiv_subscription.archive)
564 >>> view = create_initialized_view(spiv_subscription, name="+index")
565 >>> print view.label
566- Access to PPA for Celso Providelo
567+ Access to PPA named pppa for Celso Providelo
568
569 Initially the subscription does not have an active token:
570
571@@ -353,7 +353,7 @@
572 Andrew Bennetts
573
574 >>> print view.sources_list_entries.context.archive_url
575- http://spiv:...@private-ppa.launchpad.dev/cprov/ppa/...
576+ http://spiv:...@private-ppa.launchpad.dev/cprov/pppa/...
577
578 The view can also be used to regenerate the source.list entries.
579
580
581=== modified file 'lib/lp/soyuz/doc/archive.txt'
582--- lib/lp/soyuz/doc/archive.txt 2010-04-28 14:07:13 +0000
583+++ lib/lp/soyuz/doc/archive.txt 2010-04-29 19:44:31 +0000
584@@ -1531,7 +1531,7 @@
585
586 >>> archive_purposes = [archive.purpose.name for archive in archive_set]
587 >>> len(archive_purposes)
588- 20
589+ 19
590
591 >>> print sorted(set(archive_purposes))
592 ['COPY', 'DEBUG', 'PARTNER', 'PPA', 'PRIMARY']
593
594=== modified file 'lib/lp/soyuz/doc/archiveauthtoken.txt'
595--- lib/lp/soyuz/doc/archiveauthtoken.txt 2010-03-03 09:05:56 +0000
596+++ lib/lp/soyuz/doc/archiveauthtoken.txt 2010-04-29 19:44:31 +0000
597@@ -12,7 +12,7 @@
598 >>> login("admin@canonical.com")
599 >>> joe = factory.makePerson(name="joe", displayname="Joe Smith")
600 >>> joe_private_ppa = factory.makeArchive(
601- ... owner=joe, private=True)
602+ ... owner=joe, private=True, name='ppa')
603 >>> logout()
604
605 == Creating new tokens ==
606
607=== modified file 'lib/lp/soyuz/doc/sourcepackagerelease.txt'
608--- lib/lp/soyuz/doc/sourcepackagerelease.txt 2010-04-12 08:29:02 +0000
609+++ lib/lp/soyuz/doc/sourcepackagerelease.txt 2010-04-29 19:44:31 +0000
610@@ -271,7 +271,7 @@
611
612 >>> login('foo.bar@canonical.com')
613 >>> cprov_private_ppa = factory.makeArchive(
614- ... owner=cprov, private=True)
615+ ... owner=cprov, private=True, name='pppa')
616
617 >>> private_publication = test_publisher.getPubSource(
618 ... archive=cprov_private_ppa)
619@@ -283,7 +283,7 @@
620 >>> published_archives = test_sourcepackagerelease.published_archives
621 >>> for archive in published_archives:
622 ... print archive.displayname
623- PPA for Celso Providelo
624+ PPA named pppa for Celso Providelo
625
626 'foo - 666' sourcepackagerelease is only published in Celso's Private
627 PPA. So, Only Celso and administrators can get 'launchpad.View' on it.
628@@ -320,7 +320,7 @@
629 >>> for archive in published_archives:
630 ... print archive.displayname
631 Primary Archive for Ubuntu Linux
632- PPA for Celso Providelo
633+ PPA named pppa for Celso Providelo
634
635 And we can see it's publicly available now, as expected.
636
637@@ -366,5 +366,5 @@
638 >>> for archive in published_archives:
639 ... print archive.displayname
640 Primary Archive for Ubuntu Linux
641- PPA for Celso Providelo
642+ PPA named pppa for Celso Providelo
643
644
645=== modified file 'lib/lp/soyuz/model/archive.py'
646--- lib/lp/soyuz/model/archive.py 2010-04-28 14:07:13 +0000
647+++ lib/lp/soyuz/model/archive.py 2010-04-29 19:44:31 +0000
648@@ -1560,8 +1560,7 @@
649 (name, distribution.name))
650 else:
651 archive = Archive.selectOneBy(
652- owner=owner, distribution=distribution, name=name,
653- purpose=ArchivePurpose.PPA)
654+ owner=owner, name=name, purpose=ArchivePurpose.PPA)
655 if archive is not None:
656 raise AssertionError(
657 "Person '%s' already has a PPA named '%s'." %
658
659=== modified file 'lib/lp/soyuz/scripts/tests/test_ppa_add_missing_builds.py'
660--- lib/lp/soyuz/scripts/tests/test_ppa_add_missing_builds.py 2010-01-27 17:45:03 +0000
661+++ lib/lp/soyuz/scripts/tests/test_ppa_add_missing_builds.py 2010-04-29 19:44:31 +0000
662@@ -119,6 +119,7 @@
663 "-a", "i386",
664 "-a", "hppa",
665 "--owner", "%s" % self.ppa.owner.name,
666+ "--ppa", self.ppa.name,
667 ]
668 code, stdout, stderr = self.runScript(args)
669 self.assertEqual(
670
671=== modified file 'lib/lp/soyuz/stories/ppa/xx-ppa-packages.txt'
672--- lib/lp/soyuz/stories/ppa/xx-ppa-packages.txt 2010-03-08 09:27:36 +0000
673+++ lib/lp/soyuz/stories/ppa/xx-ppa-packages.txt 2010-04-29 19:44:31 +0000
674@@ -192,7 +192,7 @@
675 ... distribution=ubuntu)
676 >>> joe = factory.makePerson(name="joe")
677 >>> public_ppa = factory.makeArchive(
678- ... owner=joe, distribution=ubuntu)
679+ ... owner=joe, distribution=ubuntu, name='ppa')
680 >>> from lp.soyuz.tests.test_publishing import SoyuzTestPublisher
681 >>> test_publisher = SoyuzTestPublisher()
682 >>> test_publisher.prepareBreezyAutotest()
683
684=== modified file 'lib/lp/testing/__init__.py'
685--- lib/lp/testing/__init__.py 2010-04-24 02:41:03 +0000
686+++ lib/lp/testing/__init__.py 2010-04-29 19:44:31 +0000
687@@ -38,6 +38,7 @@
688 'validate_mock_class',
689 'WindmillTestCase',
690 'with_anonymous_login',
691+ 'ws_object'
692 'YUIUnitTestCase',
693 'ZopeTestInSubProcess',
694 ]
695@@ -79,7 +80,8 @@
696 isinstance as zope_isinstance, removeSecurityProxy)
697 from zope.testing.testrunner.runner import TestResult as ZopeTestResult
698
699-from canonical.launchpad.webapp import errorlog
700+from canonical.launchpad.webapp import canonical_url, errorlog
701+from canonical.launchpad.webapp.servers import WebServiceTestRequest
702 from canonical.config import config
703 from canonical.launchpad.webapp.interaction import ANONYMOUS
704 from canonical.launchpad.webapp.interfaces import ILaunchBag
705@@ -952,6 +954,20 @@
706 else:
707 break
708
709+
710+def ws_object(launchpad, obj):
711+ """Convert an object into its webservice version.
712+
713+ :param launchpad: The Launchpad instance to convert from.
714+ :param obj: The object to convert.
715+ :return: A launchpadlib Entry object.
716+ """
717+ api_request = WebServiceTestRequest()
718+ obj_url = canonical_url(obj, request=api_request)
719+ return launchpad.load(
720+ obj_url.replace('http://api.launchpad.dev/',
721+ str(launchpad._root_uri)))
722+
723 def unlink_source_packages(product):
724 """Remove all links between the product and source packages.
725
726
727=== modified file 'lib/lp/testing/_webservice.py'
728--- lib/lp/testing/_webservice.py 2010-04-23 17:31:49 +0000
729+++ lib/lp/testing/_webservice.py 2010-04-29 19:44:31 +0000
730@@ -11,6 +11,8 @@
731 'oauth_access_token_for',
732 ]
733
734+
735+import transaction
736 from zope.component import getUtility
737 from launchpadlib.credentials import AccessToken, Credentials
738 from launchpadlib.launchpad import Launchpad
739@@ -117,5 +119,6 @@
740 """
741 credentials = launchpadlib_credentials_for(
742 consumer_name, person, permission, context)
743+ transaction.commit()
744 version = version or Launchpad.DEFAULT_VERSION
745 return Launchpad(credentials, service_root, version=version)
746
747=== modified file 'lib/lp/testing/factory.py'
748--- lib/lp/testing/factory.py 2010-04-26 15:02:03 +0000
749+++ lib/lp/testing/factory.py 2010-04-29 19:44:31 +0000
750@@ -1637,7 +1637,7 @@
751 """Create and return a new arbitrary archive.
752
753 :param distribution: Supply IDistribution, defaults to a new one
754- made with makeDistribution().
755+ made with makeDistribution() for non-PPAs and ubuntu for PPAs.
756 :param owner: Supper IPerson, defaults to a new one made with
757 makePerson().
758 :param name: Name of the archive, defaults to a random string.
759@@ -1647,16 +1647,20 @@
760 :param virtualized: Whether the archive is virtualized.
761 :param description: A description of the archive.
762 """
763- if distribution is None:
764- distribution = self.makeDistribution()
765- if owner is None:
766- owner = self.makePerson()
767 if purpose is None:
768 purpose = ArchivePurpose.PPA
769+ if distribution is None:
770+ # See bug #568769
771+ if purpose == ArchivePurpose.PPA:
772+ distribution = getUtility(ILaunchpadCelebrities).ubuntu
773+ else:
774+ distribution = self.makeDistribution()
775+ if owner is None:
776+ owner = self.makePerson()
777 if name is None:
778- try:
779- name = default_name_by_purpose[purpose]
780- except KeyError:
781+ if purpose != ArchivePurpose.PPA:
782+ name = default_name_by_purpose.get(purpose)
783+ if name is None:
784 name = self.getUniqueString()
785
786 # Making a distribution makes an archive, and there can be only one