Merge lp:~wallyworld/launchpad/recipe-webservice-api into lp:launchpad

Proposed by Ian Booth
Status: Merged
Approved by: Ian Booth
Approved revision: no longer in the source branch.
Merged at revision: 12459
Proposed branch: lp:~wallyworld/launchpad/recipe-webservice-api
Merge into: lp:launchpad
Diff against target: 758 lines (+201/-99)
20 files modified
lib/canonical/launchpad/interfaces/_schema_circular_imports.py (+17/-0)
lib/lp/code/browser/branch.py (+1/-1)
lib/lp/code/browser/sourcepackagerecipe.py (+3/-3)
lib/lp/code/browser/sourcepackagerecipelisting.py (+2/-2)
lib/lp/code/browser/tests/test_sourcepackagerecipe.py (+13/-11)
lib/lp/code/interfaces/hasrecipes.py (+12/-3)
lib/lp/code/interfaces/sourcepackagerecipe.py (+34/-9)
lib/lp/code/model/branch.py (+3/-2)
lib/lp/code/model/sourcepackagerecipe.py (+36/-19)
lib/lp/code/model/tests/test_hasrecipes.py (+9/-15)
lib/lp/code/model/tests/test_sourcepackagerecipe.py (+50/-14)
lib/lp/code/scripts/tests/test_request_daily_builds.py (+5/-11)
lib/lp/code/stories/webservice/xx-branch.txt (+2/-1)
lib/lp/code/templates/sourcepackagerecipe-listing.pt (+1/-1)
lib/lp/codehosting/scanner/bzrsync.py (+1/-1)
lib/lp/registry/model/person.py (+4/-3)
lib/lp/registry/model/product.py (+2/-1)
lib/lp/registry/stories/webservice/xx-person.txt (+2/-0)
lib/lp/registry/stories/webservice/xx-project-registry.txt (+2/-0)
lib/lp/registry/tests/test_person.py (+2/-2)
To merge this branch: bzr merge lp:~wallyworld/launchpad/recipe-webservice-api
Reviewer Review Type Date Requested Status
Leonard Richardson (community) Approve
Review via email: mp+50684@code.launchpad.net

Commit message

[r=leonardr][bug=712329,712331] Add extra recipe methods to the web service.

Description of the change

Simple change to export via the webservice some recipe apis to list builds and recipes. See bugs 712329 and 712331.

== Implementation ==

Initial review discussions indicated that the current methods getBuilds() and getPendingBuilds() were poorly named for exporting. So the following methods were introduced:

getBuilds() - new, returns all builds
getPendingBuilds() - unchanged, returns builds where status == needsbuild
getCompleteBuilds() - renamed from getBuilds(), returns builds where status != needsbuild

Add required decorators to the exported methods:
IHasRecipe.getRecipes()
ISourcePackageRecipe.getBuilds()
ISourcePackageRecipe.getCurrentBuilds()
ISourcePackageRecipe.getPendingBuilds()
ISourcePackageRecipe.getLastBuild()

An additional small change was made to the order by statement used by the queries. The secondary order by term, BuildFarmJob.id, was changed to Desc() so that the semantics match the order imposed by the start_date, create_date ordering ie later builds go first

== Tests ==

Add/modify tests in test_sourcepackagerecipe.TestWebservice
  test_getBuilds
  test_getRecipes
  test_requestBuild

bin/test -vvt test_sourcepackagerecipe.TestWebservice

== Lint ==
Linting changed files:
  lib/canonical/launchpad/interfaces/_schema_circular_imports.py
  lib/lp/code/interfaces/hasrecipes.py
  lib/lp/code/interfaces/sourcepackagerecipe.py
  lib/lp/code/model/tests/test_sourcepackagerecipe.py

./lib/canonical/launchpad/interfaces/_schema_circular_imports.py
     551: E501 line too long (82 characters)
     551: Line exceeds 78 characters.
./lib/lp/code/interfaces/hasrecipes.py
      21: E302 expected 2 blank lines, found 1
./lib/lp/code/interfaces/sourcepackagerecipe.py
     185: E501 line too long (83 characters)
     185: Line exceeds 78 characters.

To post a comment you must log in.
Revision history for this message
Robert Collins (lifeless) wrote :

Just to mention, this seems gratuitiously different to the existing api for getBuildRecords, which we could expose on your context - conceptually an IHasBuildRecords or similar. Is there a reason for this?

Revision history for this message
Ian Booth (wallyworld) wrote :

Hi Robert

I was implementing what the bug report (712331) asked for ie publish
ISourcePackageRecipe.getBuilds() via the web service api.

getBuildRecords() certainly is very different as you say, however this
method is exposed via the web service already.

On 22/02/11 16:54, Robert Collins wrote:
> Just to mention, this seems gratuitiously different to the existing api for getBuildRecords, which we could expose on your context - conceptually an IHasBuildRecords or similar. Is there a reason for this?

Revision history for this message
Leonard Richardson (leonardr) wrote :

requestBuild is a classic case of a named operation, but there's no reason to publish the others as named operations.
They don't change the dataset and they don't take any arguments. Let's publish them as scoped collections and keep our public API cleaner:

recipes
builds
completed_builds
pending_builds

And a link:

last_build

To do this, define fields by these names in the interfaces, and implement them with @property method definitions that invoke the method behind the scenes.

@property
def recipes(self):
    return self.getRecipes()

Or, if possible, get rid of getRecipes() altogether, and just implement .recipes for both the internal and external APIs. So 'recipes' would contain the getRecipes() implementation instead of invoking getRecipes().

review: Needs Fixing
Revision history for this message
Ian Booth (wallyworld) wrote :

> requestBuild is a classic case of a named operation, but there's no reason to
> publish the others as named operations.
> They don't change the dataset and they don't take any arguments. Let's publish
> them as scoped collections and keep our public API cleaner:
>
> recipes
> builds
> completed_builds
> pending_builds
>
> And a link:
>
> last_build
>

Hi Leonard,

I have converted the getter methods to be properties as discussed. Tests pass and running up an instance locally works fine too. I ran some additional tests: test_person, test_hasrecipes, test_request_daily_builds, since these were changed as part of the conversion to using properties.

Revision history for this message
Leonard Richardson (leonardr) wrote :

This looks great now. My only comment is that "sorted by 'desc(date_created), then by desc(id).'" is a bad way to describe a field. This description will be going into the HTML documentation at https://launchpad.net/+apidoc/devel.html, so you should say something understandable by external developers. Something like "Sorted in descending order of creation."

review: Approve
Revision history for this message
Robert Collins (lifeless) wrote :

On Thu, Feb 24, 2011 at 2:18 AM, Leonard Richardson
<email address hidden> wrote:
> Review: Approve
> This looks great now. My only comment is that  "sorted by 'desc(date_created), then by desc(id).'" is a bad way to describe a field. This description will be going into the HTML documentation at https://launchpad.net/+apidoc/devel.html, so you should say something understandable by external developers. Something like "Sorted in descending order of creation."

An interesting thing about phrasing it like that, is that it makes it
obvious that the sort is inefficient: better to just sort on Desc(id)
- that is precisely the same sort because ids are allocated
sequentially, and under half the work for the DB.

Revision history for this message
Ian Booth (wallyworld) wrote :

>
> An interesting thing about phrasing it like that, is that it makes it
> obvious that the sort is inefficient: better to just sort on Desc(id)
> - that is precisely the same sort because ids are allocated
> sequentially, and under half the work for the DB.

Done.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/canonical/launchpad/interfaces/_schema_circular_imports.py'
2--- lib/canonical/launchpad/interfaces/_schema_circular_imports.py 2011-02-02 09:50:58 +0000
3+++ lib/canonical/launchpad/interfaces/_schema_circular_imports.py 2011-02-24 23:45:43 +0000
4@@ -74,6 +74,7 @@
5 IHasMergeProposals,
6 IHasRequestedReviews,
7 )
8+from lp.code.interfaces.hasrecipes import IHasRecipes
9 from lp.code.interfaces.sourcepackagerecipe import ISourcePackageRecipe
10 from lp.code.interfaces.sourcepackagerecipebuild import (
11 ISourcePackageRecipeBuild,
12@@ -262,6 +263,10 @@
13
14 patch_entry_return_type(IPerson, 'getRecipe', ISourcePackageRecipe)
15
16+# IHasRecipe
17+patch_collection_property(
18+ IHasRecipes, 'recipes', ISourcePackageRecipe)
19+
20 IPerson['hardware_submissions'].value_type.schema = IHWSubmission
21
22 # publishing.py
23@@ -440,6 +445,18 @@
24 ISourcePackageRelease, 'source_package_recipe_build',
25 ISourcePackageRecipeBuild)
26
27+# ISourcePackageRecipeView
28+patch_entry_return_type(
29+ ISourcePackageRecipe, 'requestBuild', ISourcePackageRecipeBuild)
30+patch_reference_property(
31+ ISourcePackageRecipe, 'last_build', ISourcePackageRecipeBuild)
32+patch_collection_property(
33+ ISourcePackageRecipe, 'builds', ISourcePackageRecipeBuild)
34+patch_collection_property(
35+ ISourcePackageRecipe, 'pending_builds', ISourcePackageRecipeBuild)
36+patch_collection_property(
37+ ISourcePackageRecipe, 'completed_builds', ISourcePackageRecipeBuild)
38+
39 # IHasBugs
40 patch_plain_parameter_type(
41 IHasBugs, 'searchTasks', 'assignee', IPerson)
42
43=== modified file 'lib/lp/code/browser/branch.py'
44--- lib/lp/code/browser/branch.py 2011-02-17 03:29:50 +0000
45+++ lib/lp/code/browser/branch.py 2011-02-24 23:45:43 +0000
46@@ -548,7 +548,7 @@
47
48 @property
49 def recipe_count_text(self):
50- count = self.context.getRecipes().count()
51+ count = self.context.recipes.count()
52 if count == 0:
53 return 'No recipes'
54 elif count == 1:
55
56=== modified file 'lib/lp/code/browser/sourcepackagerecipe.py'
57--- lib/lp/code/browser/sourcepackagerecipe.py 2011-02-23 02:08:58 +0000
58+++ lib/lp/code/browser/sourcepackagerecipe.py 2011-02-24 23:45:43 +0000
59@@ -299,8 +299,8 @@
60 This allows started but unfinished builds to show up in the view but
61 be discarded as more recent builds become available.
62 """
63- builds = list(recipe.getPendingBuilds())
64- for build in recipe.getBuilds():
65+ builds = list(recipe.pending_builds)
66+ for build in recipe.completed_builds:
67 builds.append(build)
68 if len(builds) >= 5:
69 break
70@@ -328,7 +328,7 @@
71 The distroseries function as defaults for requesting a build.
72 """
73 initial_values = {'distros': self.context.distroseries}
74- build = self.context.getLastBuild()
75+ build = self.context.last_build
76 if build is not None:
77 initial_values['archive'] = build.archive
78 return initial_values
79
80=== modified file 'lib/lp/code/browser/sourcepackagerecipelisting.py'
81--- lib/lp/code/browser/sourcepackagerecipelisting.py 2011-02-17 03:17:54 +0000
82+++ lib/lp/code/browser/sourcepackagerecipelisting.py 2011-02-24 23:45:43 +0000
83@@ -29,7 +29,7 @@
84 def view_recipes(self):
85 text = 'View source package recipes'
86 enabled = False
87- if self.context.getRecipes().count():
88+ if self.context.recipes.count():
89 enabled = True
90 if not getFeatureFlag(RECIPE_ENABLED_FLAG):
91 enabled = False
92@@ -51,7 +51,7 @@
93
94 def initialize(self):
95 super(RecipeListingView, self).initialize()
96- recipes = self.context.getRecipes()
97+ recipes = self.context.recipes
98 if recipes.count() == 1:
99 recipe = recipes.one()
100 self.request.response.redirect(canonical_url(recipe))
101
102=== modified file 'lib/lp/code/browser/tests/test_sourcepackagerecipe.py'
103--- lib/lp/code/browser/tests/test_sourcepackagerecipe.py 2011-02-22 23:07:17 +0000
104+++ lib/lp/code/browser/tests/test_sourcepackagerecipe.py 2011-02-24 23:45:43 +0000
105@@ -1194,8 +1194,10 @@
106 def test_builds(self):
107 """Ensure SourcePackageRecipeView.builds is as described."""
108 recipe = self.makeRecipe()
109+ # We create builds in time ascending order (oldest first) since we
110+ # use id as the ordering attribute and lower ids mean created earlier.
111 date_gen = time_counter(
112- datetime(2010, 03, 16, tzinfo=utc), timedelta(days=-1))
113+ datetime(2010, 03, 16, tzinfo=utc), timedelta(days=1))
114 build1 = self.makeBuildJob(recipe, date_gen.next())
115 build2 = self.makeBuildJob(recipe, date_gen.next())
116 build3 = self.makeBuildJob(recipe, date_gen.next())
117@@ -1204,7 +1206,7 @@
118 build6 = self.makeBuildJob(recipe, date_gen.next())
119 view = SourcePackageRecipeView(recipe, None)
120 self.assertEqual(
121- [build1, build2, build3, build4, build5, build6],
122+ [build6, build5, build4, build3, build2, build1],
123 view.builds)
124
125 def set_status(build, status):
126@@ -1214,19 +1216,19 @@
127 if status == BuildStatus.FULLYBUILT:
128 naked_build.date_finished = (
129 naked_build.date_created + timedelta(minutes=10))
130- set_status(build1, BuildStatus.FULLYBUILT)
131- set_status(build2, BuildStatus.FAILEDTOBUILD)
132+ set_status(build6, BuildStatus.FULLYBUILT)
133+ set_status(build5, BuildStatus.FAILEDTOBUILD)
134 # When there are 4+ pending builds, only the the most
135 # recently-completed build is returned (i.e. build1, not build2)
136 self.assertEqual(
137- [build3, build4, build5, build6, build1],
138+ [build4, build3, build2, build1, build6],
139 view.builds)
140+ set_status(build4, BuildStatus.FULLYBUILT)
141 set_status(build3, BuildStatus.FULLYBUILT)
142- set_status(build4, BuildStatus.FULLYBUILT)
143- set_status(build5, BuildStatus.FULLYBUILT)
144- set_status(build6, BuildStatus.FULLYBUILT)
145+ set_status(build2, BuildStatus.FULLYBUILT)
146+ set_status(build1, BuildStatus.FULLYBUILT)
147 self.assertEqual(
148- [build1, build2, build3, build4, build5], view.builds)
149+ [build6, build5, build4, build3, build2], view.builds)
150
151 def test_request_daily_builds_button_stale(self):
152 # Recipes that are stale and are built daily have a build now link
153@@ -1300,7 +1302,7 @@
154 browser = self.getViewBrowser(recipe)
155 browser.getControl('Build now').click()
156 login(ANONYMOUS)
157- builds = recipe.getPendingBuilds()
158+ builds = recipe.pending_builds
159 build_distros = [
160 build.distroseries.displayname for build in builds]
161 build_distros.sort()
162@@ -1346,7 +1348,7 @@
163 browser.getControl('Request builds').click()
164
165 login(ANONYMOUS)
166- builds = recipe.getPendingBuilds()
167+ builds = recipe.pending_builds
168 build_distros = [
169 build.distroseries.displayname for build in builds]
170 build_distros.sort()
171
172=== modified file 'lib/lp/code/interfaces/hasrecipes.py'
173--- lib/lp/code/interfaces/hasrecipes.py 2010-08-20 20:31:18 +0000
174+++ lib/lp/code/interfaces/hasrecipes.py 2011-02-24 23:45:43 +0000
175@@ -10,13 +10,22 @@
176
177
178 from zope.interface import (
179- Attribute,
180 Interface,
181 )
182
183+from lazr.restful.declarations import exported
184+from lazr.restful.fields import (
185+ CollectionField,
186+ Reference,
187+ )
188+
189+from canonical.launchpad import _
190
191 class IHasRecipes(Interface):
192 """An object that has recipes."""
193
194- def getRecipes():
195- """Returns all recipes associated with the object."""
196+ recipes = exported(
197+ CollectionField(
198+ title=_("All recipes associated with the object."),
199+ value_type=Reference(schema=Interface),
200+ readonly=True))
201
202=== modified file 'lib/lp/code/interfaces/sourcepackagerecipe.py'
203--- lib/lp/code/interfaces/sourcepackagerecipe.py 2011-02-22 23:07:17 +0000
204+++ lib/lp/code/interfaces/sourcepackagerecipe.py 2011-02-24 23:45:43 +0000
205@@ -29,6 +29,7 @@
206 mutator_for,
207 operation_for_version,
208 operation_parameters,
209+ operation_returns_entry,
210 REQUEST_USER,
211 )
212 from lazr.restful.fields import (
213@@ -104,6 +105,38 @@
214
215 recipe_text = exported(Text(readonly=True))
216
217+ pending_builds = exported(
218+ CollectionField(
219+ title=_("The pending builds of this recipe."),
220+ description=_('Pending builds of this recipe, sorted in '
221+ 'descending order of creation.'),
222+ value_type=Reference(schema=Interface),
223+ readonly=True))
224+
225+ completed_builds = exported(
226+ CollectionField(
227+ title=_("The completed builds of this recipe."),
228+ description=_('Completed builds of this recipe, sorted in '
229+ 'descending order of finishing (or starting if not'
230+ 'completed successfully).'),
231+ value_type=Reference(schema=Interface),
232+ readonly=True))
233+
234+ builds = exported(
235+ CollectionField(
236+ title=_("All builds of this recipe."),
237+ description=_('All builds of this recipe, sorted in '
238+ 'descending order of finishing (or starting if not'
239+ 'completed successfully).'),
240+ value_type=Reference(schema=Interface),
241+ readonly=True))
242+
243+ last_build = exported(
244+ Reference(
245+ Interface,
246+ title=_("The the most recent build of this recipe."),
247+ readonly=True))
248+
249 def isOverQuota(requester, distroseries):
250 """True if the recipe/requester/distroseries combo is >= quota.
251
252@@ -111,20 +144,12 @@
253 :param distroseries: The distroseries to build for.
254 """
255
256- def getBuilds():
257- """Return a ResultSet of all the non-pending builds."""
258-
259- def getPendingBuilds():
260- """Return a ResultSet of all the pending builds."""
261-
262- def getLastBuild():
263- """Return the the most recent build of this recipe."""
264-
265 @call_with(requester=REQUEST_USER)
266 @operation_parameters(
267 archive=Reference(schema=IArchive),
268 distroseries=Reference(schema=IDistroSeries),
269 pocket=Choice(vocabulary=PackagePublishingPocket,))
270+ @operation_returns_entry(Interface)
271 @export_write_operation()
272 def requestBuild(archive, distroseries, requester, pocket):
273 """Request that the recipe be built in to the specified archive.
274
275=== modified file 'lib/lp/code/model/branch.py'
276--- lib/lp/code/model/branch.py 2011-01-25 18:59:18 +0000
277+++ lib/lp/code/model/branch.py 2011-02-24 23:45:43 +0000
278@@ -665,7 +665,7 @@
279 map(ClearOfficialPackageBranch, series_set.findForBranch(self)))
280 deletion_operations.extend(
281 DeletionCallable.forSourcePackageRecipe(recipe)
282- for recipe in self.getRecipes())
283+ for recipe in self.recipes)
284 return (alteration_operations, deletion_operations)
285
286 def deletionRequirements(self):
287@@ -1160,7 +1160,8 @@
288 user, checked_branches)
289 return can_access
290
291- def getRecipes(self):
292+ @property
293+ def recipes(self):
294 """See `IHasRecipes`."""
295 from lp.code.model.sourcepackagerecipedata import (
296 SourcePackageRecipeData)
297
298=== modified file 'lib/lp/code/model/sourcepackagerecipe.py'
299--- lib/lp/code/model/sourcepackagerecipe.py 2011-02-19 00:30:04 +0000
300+++ lib/lp/code/model/sourcepackagerecipe.py 2011-02-24 23:45:43 +0000
301@@ -295,34 +295,51 @@
302 continue
303 return builds
304
305- def getBuilds(self):
306- """See `ISourcePackageRecipe`."""
307- where_clause = BuildFarmJob.status != BuildStatus.NEEDSBUILD
308- order_by = Desc(Greatest(
309- BuildFarmJob.date_started,
310- BuildFarmJob.date_finished)), BuildFarmJob.id
311- return self._getBuilds(where_clause, order_by)
312-
313- def getPendingBuilds(self):
314- """See `ISourcePackageRecipe`."""
315- where_clause = BuildFarmJob.status == BuildStatus.NEEDSBUILD
316- order_by = Desc(BuildFarmJob.date_created), BuildFarmJob.id
317- return self._getBuilds(where_clause, order_by)
318-
319- def _getBuilds(self, where_clause, order_by):
320+ @property
321+ def builds(self):
322+ """See `ISourcePackageRecipe`."""
323+ order_by = (Desc(Greatest(
324+ BuildFarmJob.date_started,
325+ BuildFarmJob.date_finished)),
326+ Desc(BuildFarmJob.date_created), Desc(BuildFarmJob.id))
327+ return self._getBuilds(None, order_by)
328+
329+ @property
330+ def completed_builds(self):
331+ """See `ISourcePackageRecipe`."""
332+ filter_term = BuildFarmJob.status != BuildStatus.NEEDSBUILD
333+ order_by = (Desc(Greatest(
334+ BuildFarmJob.date_started,
335+ BuildFarmJob.date_finished)),
336+ Desc(BuildFarmJob.id))
337+ return self._getBuilds(filter_term, order_by)
338+
339+ @property
340+ def pending_builds(self):
341+ """See `ISourcePackageRecipe`."""
342+ filter_term = BuildFarmJob.status == BuildStatus.NEEDSBUILD
343+ # We want to order by date_created but this is the same as ordering
344+ # by id (since id increases monotonically) and is less expensive.
345+ order_by = Desc(BuildFarmJob.id)
346+ return self._getBuilds(filter_term, order_by)
347+
348+ def _getBuilds(self, filter_term, order_by):
349 """The actual query to get the builds."""
350- result = Store.of(self).find(
351- SourcePackageRecipeBuild,
352+ query_args = [
353 SourcePackageRecipeBuild.recipe==self,
354 SourcePackageRecipeBuild.package_build_id == PackageBuild.id,
355 PackageBuild.build_farm_job_id == BuildFarmJob.id,
356 And(PackageBuild.archive_id == Archive.id,
357 Archive._enabled == True),
358- where_clause)
359+ ]
360+ if filter_term is not None:
361+ query_args.append(filter_term)
362+ result = Store.of(self).find(SourcePackageRecipeBuild, *query_args)
363 result.order_by(order_by)
364 return result
365
366- def getLastBuild(self):
367+ @property
368+ def last_build(self):
369 """See `ISourcePackageRecipeBuild`."""
370 return self._getBuilds(
371 True, Desc(BuildFarmJob.date_finished)).first()
372
373=== modified file 'lib/lp/code/model/tests/test_hasrecipes.py'
374--- lib/lp/code/model/tests/test_hasrecipes.py 2010-10-04 19:50:45 +0000
375+++ lib/lp/code/model/tests/test_hasrecipes.py 2011-02-24 23:45:43 +0000
376@@ -7,8 +7,6 @@
377
378 __metaclass__ = type
379
380-import unittest
381-
382 from canonical.testing.layers import DatabaseFunctionalLayer
383 from lp.code.interfaces.hasrecipes import IHasRecipes
384 from lp.testing import TestCaseWithFactory
385@@ -24,16 +22,16 @@
386 branch = self.factory.makeBranch()
387 self.assertProvides(branch, IHasRecipes)
388
389- def test_branch_getRecipes(self):
390+ def test_branch_recipes(self):
391 # IBranch.recipes should provide all the SourcePackageRecipes attached
392 # to that branch.
393 base_branch = self.factory.makeBranch()
394 recipe1 = self.factory.makeSourcePackageRecipe(branches=[base_branch])
395 recipe2 = self.factory.makeSourcePackageRecipe(branches=[base_branch])
396 recipe_ignored = self.factory.makeSourcePackageRecipe()
397- self.assertEqual(2, base_branch.getRecipes().count())
398+ self.assertEqual(2, base_branch.recipes.count())
399
400- def test_branch_getRecipes_nonbase(self):
401+ def test_branch_recipes_nonbase(self):
402 # IBranch.recipes should provide all the SourcePackageRecipes
403 # that refer to the branch, even as a non-base branch.
404 base_branch = self.factory.makeBranch()
405@@ -41,28 +39,28 @@
406 recipe = self.factory.makeSourcePackageRecipe(
407 branches=[base_branch, nonbase_branch])
408 recipe_ignored = self.factory.makeSourcePackageRecipe()
409- self.assertEqual(recipe, nonbase_branch.getRecipes().one())
410+ self.assertEqual(recipe, nonbase_branch.recipes.one())
411
412 def test_person_implements_hasrecipes(self):
413 # Person should implement IHasRecipes.
414 person = self.factory.makeBranch()
415 self.assertProvides(person, IHasRecipes)
416
417- def test_person_getRecipes(self):
418- # IPerson.getRecipes should provide all the SourcePackageRecipes
419+ def test_person_recipes(self):
420+ # IPerson.recipes should provide all the SourcePackageRecipes
421 # owned by that person.
422 person = self.factory.makePerson()
423 recipe1 = self.factory.makeSourcePackageRecipe(owner=person)
424 recipe2 = self.factory.makeSourcePackageRecipe(owner=person)
425 recipe_ignored = self.factory.makeSourcePackageRecipe()
426- self.assertEqual(2, person.getRecipes().count())
427+ self.assertEqual(2, person.recipes.count())
428
429 def test_product_implements_hasrecipes(self):
430 # Product should implement IHasRecipes.
431 product = self.factory.makeProduct()
432 self.assertProvides(product, IHasRecipes)
433
434- def test_product_getRecipes(self):
435+ def test_product_recipes(self):
436 # IProduct.recipes should provide all the SourcePackageRecipes
437 # attached to that product's branches.
438 product = self.factory.makeProduct()
439@@ -70,8 +68,4 @@
440 recipe1 = self.factory.makeSourcePackageRecipe(branches=[branch])
441 recipe2 = self.factory.makeSourcePackageRecipe(branches=[branch])
442 recipe_ignored = self.factory.makeSourcePackageRecipe()
443- self.assertEqual(2, product.getRecipes().count())
444-
445-
446-def test_suite():
447- return unittest.TestLoader().loadTestsFromName(__name__)
448+ self.assertEqual(2, product.recipes.count())
449
450=== modified file 'lib/lp/code/model/tests/test_sourcepackagerecipe.py'
451--- lib/lp/code/model/tests/test_sourcepackagerecipe.py 2011-02-18 04:25:00 +0000
452+++ lib/lp/code/model/tests/test_sourcepackagerecipe.py 2011-02-24 23:45:43 +0000
453@@ -10,7 +10,6 @@
454 timedelta,
455 )
456 import textwrap
457-import unittest
458
459 from bzrlib.plugins.builder.recipe import ForbiddenInstructionError
460 from pytz import UTC
461@@ -582,19 +581,32 @@
462 timedelta(minutes=11), recipe.getMedianBuildDuration())
463
464 def test_getBuilds(self):
465- # Builds that need building are pending.
466+ # Test the various getBuilds methods.
467 recipe = self.factory.makeSourcePackageRecipe()
468- build = self.factory.makeSourcePackageRecipeBuild(recipe=recipe)
469- self.assertEqual([], list(recipe.getBuilds()))
470- self.assertEqual([build], list(recipe.getPendingBuilds()))
471+ builds = [
472+ self.factory.makeSourcePackageRecipeBuild(recipe=recipe)
473+ for x in range(3)]
474+ # We want the latest builds first.
475+ builds.reverse()
476+
477+ self.assertEqual([], list(recipe.completed_builds))
478+ self.assertEqual(builds, list(recipe.pending_builds))
479+ self.assertEqual(builds, list(recipe.builds))
480+
481+ # Change the status of one of the builds and retest.
482+ removeSecurityProxy(builds[0]).status = BuildStatus.FULLYBUILT
483+ self.assertEqual([builds[0]], list(recipe.completed_builds))
484+ self.assertEqual(builds[1:], list(recipe.pending_builds))
485+ self.assertEqual(builds, list(recipe.builds))
486
487 def test_getBuilds_cancelled(self):
488 # Cancelled builds are not considered pending.
489 recipe = self.factory.makeSourcePackageRecipe()
490 build = self.factory.makeSourcePackageRecipeBuild(recipe=recipe)
491 build.cancelBuild()
492- self.assertEqual([build], list(recipe.getBuilds()))
493- self.assertEqual([], list(recipe.getPendingBuilds()))
494+ self.assertEqual([build], list(recipe.builds))
495+ self.assertEqual([build], list(recipe.completed_builds))
496+ self.assertEqual([], list(recipe.pending_builds))
497
498 def test_setRecipeText_private_base_branch(self):
499 source_package_recipe = self.factory.makeSourcePackageRecipe()
500@@ -633,8 +645,9 @@
501 recipe=recipe, archive=archive)
502 with person_logged_in(archive.owner):
503 archive.disable()
504- self.assertEqual([], list(recipe.getBuilds()))
505- self.assertEqual([], list(recipe.getPendingBuilds()))
506+ self.assertEqual([], list(recipe.builds))
507+ self.assertEqual([], list(recipe.completed_builds))
508+ self.assertEqual([], list(recipe.pending_builds))
509
510
511 class TestRecipeBranchRoundTripping(TestCaseWithFactory):
512@@ -901,8 +914,14 @@
513 recipe, user = self.makeRecipe()[:-1]
514 self.assertEqual(recipe, user.getRecipe(name=recipe.name))
515
516+ def test_recipes(self):
517+ """Person.recipes works as expected."""
518+ recipe, user = self.makeRecipe()[:-1]
519+ [ws_recipe] = user.recipes
520+ self.assertEqual(recipe, ws_recipe)
521+
522 def test_requestBuild(self):
523- """Build requests can be performed."""
524+ """Build requests can be performed and last_build works."""
525 person = self.factory.makePerson()
526 archive = self.factory.makeArchive(owner=person)
527 distroseries = self.factory.makeSourcePackageRecipeDistroseries()
528@@ -910,9 +929,10 @@
529 recipe, user, launchpad = self.makeRecipe(person)
530 distroseries = ws_object(launchpad, distroseries)
531 archive = ws_object(launchpad, archive)
532- recipe.requestBuild(
533+ build = recipe.requestBuild(
534 archive=archive, distroseries=distroseries,
535 pocket=PackagePublishingPocket.RELEASE.title)
536+ self.assertEqual(build, recipe.last_build)
537
538 def test_requestBuildRejectRepeat(self):
539 """Build requests are rejected if already pending."""
540@@ -967,6 +987,22 @@
541 pocket=PackagePublishingPocket.RELEASE.title)
542 self.assertIn('BuildNotAllowedForDistro', str(e))
543
544-
545-def test_suite():
546- return unittest.TestLoader().loadTestsFromName(__name__)
547+ def test_getBuilds(self):
548+ """SourcePackageRecipe.[pending_|completed_]builds is as expected."""
549+ person = self.factory.makePerson()
550+ archives = [self.factory.makeArchive(owner=person) for x in range(4)]
551+ distroseries= self.factory.makeSourcePackageRecipeDistroseries()
552+
553+ recipe, user, launchpad = self.makeRecipe(person)
554+ distroseries = ws_object(launchpad, distroseries)
555+
556+ builds = []
557+ for archive in archives:
558+ archive = ws_object(launchpad, archive)
559+ build = recipe.requestBuild(
560+ archive=archive, distroseries=distroseries,
561+ pocket=PackagePublishingPocket.RELEASE.title)
562+ builds.insert(0, build)
563+ self.assertEqual(builds, list(recipe.pending_builds))
564+ self.assertEqual(builds, list(recipe.builds))
565+ self.assertEqual([], list(recipe.completed_builds))
566
567=== modified file 'lib/lp/code/scripts/tests/test_request_daily_builds.py'
568--- lib/lp/code/scripts/tests/test_request_daily_builds.py 2011-01-13 03:53:53 +0000
569+++ lib/lp/code/scripts/tests/test_request_daily_builds.py 2011-02-24 23:45:43 +0000
570@@ -5,8 +5,6 @@
571
572 """Test the request_daily_builds script"""
573
574-import unittest
575-
576 import transaction
577
578 from canonical.launchpad.scripts.tests import run_script
579@@ -28,14 +26,14 @@
580 pack_branch = self.factory.makePackageBranch()
581 pack_recipe = self.factory.makeSourcePackageRecipe(
582 build_daily=True, is_stale=True, branches=[pack_branch])
583- self.assertEqual(0, prod_recipe.getPendingBuilds().count())
584- self.assertEqual(0, pack_recipe.getPendingBuilds().count())
585+ self.assertEqual(0, prod_recipe.pending_builds.count())
586+ self.assertEqual(0, pack_recipe.pending_builds.count())
587 transaction.commit()
588 retcode, stdout, stderr = run_script(
589 'cronscripts/request_daily_builds.py', [])
590 self.assertIn('Requested 2 daily builds.', stderr)
591- self.assertEqual(1, prod_recipe.getPendingBuilds().count())
592- self.assertEqual(1, pack_recipe.getPendingBuilds().count())
593+ self.assertEqual(1, prod_recipe.pending_builds.count())
594+ self.assertEqual(1, pack_recipe.pending_builds.count())
595 self.assertFalse(prod_recipe.is_stale)
596 self.assertFalse(pack_recipe.is_stale)
597
598@@ -47,13 +45,9 @@
599 transaction.commit()
600 retcode, stdout, stderr = run_script(
601 'cronscripts/request_daily_builds.py', [])
602- self.assertEqual(0, recipe.getPendingBuilds().count())
603+ self.assertEqual(0, recipe.pending_builds.count())
604 self.assertIn('Requested 0 daily builds.', stderr)
605 utility = ErrorReportingUtility()
606 utility.configure('request_daily_builds')
607 oops = utility.getLastOopsReport()
608 self.assertIn('NonPPABuildRequest', oops.tb_text)
609-
610-
611-def test_suite():
612- return unittest.TestLoader().loadTestsFromName(__name__)
613
614=== modified file 'lib/lp/code/stories/webservice/xx-branch.txt'
615--- lib/lp/code/stories/webservice/xx-branch.txt 2011-01-31 17:08:35 +0000
616+++ lib/lp/code/stories/webservice/xx-branch.txt 2011-02-24 23:45:43 +0000
617@@ -130,6 +130,7 @@
618 owner_link: u'.../~eric'
619 private: False
620 project_link: u'.../fooix'
621+ recipes_collection_link: u'http://.../~eric/fooix/trunk/recipes'
622 registrant_link: u'.../~eric'
623 repository_format: None
624 resource_type_link: u'.../#branch'
625@@ -282,7 +283,7 @@
626 >>> print deletable
627 False
628
629- Deleting only works on branches that do not have anything else
630+ Deleting only works on branches that do not have anything else
631 depending on them.
632
633 >>> response = webservice.delete('/~eric/fooix/feature-branch')
634
635=== modified file 'lib/lp/code/templates/sourcepackagerecipe-listing.pt'
636--- lib/lp/code/templates/sourcepackagerecipe-listing.pt 2010-07-26 20:29:19 +0000
637+++ lib/lp/code/templates/sourcepackagerecipe-listing.pt 2011-02-24 23:45:43 +0000
638@@ -20,7 +20,7 @@
639 </tr>
640 </thead>
641 <tbody>
642- <tal:recipes repeat="recipe context/getRecipes">
643+ <tal:recipes repeat="recipe context/recipes">
644 <tr>
645 <td colspan="2">
646 <a tal:attributes="href recipe/fmt:url"
647
648=== modified file 'lib/lp/codehosting/scanner/bzrsync.py'
649--- lib/lp/codehosting/scanner/bzrsync.py 2010-10-15 18:27:58 +0000
650+++ lib/lp/codehosting/scanner/bzrsync.py 2011-02-24 23:45:43 +0000
651@@ -319,5 +319,5 @@
652
653
654 def update_recipes(tip_changed):
655- for recipe in tip_changed.db_branch.getRecipes():
656+ for recipe in tip_changed.db_branch.recipes:
657 recipe.is_stale = True
658
659=== modified file 'lib/lp/registry/model/person.py'
660--- lib/lp/registry/model/person.py 2011-02-16 11:03:04 +0000
661+++ lib/lp/registry/model/person.py 2011-02-24 23:45:43 +0000
662@@ -2851,7 +2851,8 @@
663 from lp.hardwaredb.model.hwdb import HWSubmissionSet
664 return HWSubmissionSet().search(owner=self)
665
666- def getRecipes(self):
667+ @property
668+ def recipes(self):
669 """See `IHasRecipes`."""
670 from lp.code.model.sourcepackagerecipe import SourcePackageRecipe
671 store = Store.of(self)
672@@ -3454,8 +3455,8 @@
673
674 def _mergeSourcePackageRecipes(self, from_person, to_person):
675 # This shouldn't use removeSecurityProxy.
676- recipes = from_person.getRecipes()
677- existing_names = [r.name for r in to_person.getRecipes()]
678+ recipes = from_person.recipes
679+ existing_names = [r.name for r in to_person.recipes]
680 for recipe in recipes:
681 new_name = recipe.name
682 count = 1
683
684=== modified file 'lib/lp/registry/model/product.py'
685--- lib/lp/registry/model/product.py 2011-01-24 20:53:10 +0000
686+++ lib/lp/registry/model/product.py 2011-02-24 23:45:43 +0000
687@@ -1315,7 +1315,8 @@
688 return DecoratedResultSet(
689 self.getVersionSortedSeries(statuses=statuses), decorate)
690
691- def getRecipes(self):
692+ @property
693+ def recipes(self):
694 """See `IHasRecipes`."""
695 from lp.code.model.branch import Branch
696 store = Store.of(self)
697
698=== modified file 'lib/lp/registry/stories/webservice/xx-person.txt'
699--- lib/lp/registry/stories/webservice/xx-person.txt 2011-02-13 22:54:48 +0000
700+++ lib/lp/registry/stories/webservice/xx-person.txt 2011-02-24 23:45:43 +0000
701@@ -50,6 +50,7 @@
702 u'http://.../~salgado/+email/guilherme.salgado@canonical.com'
703 private: False
704 proposed_members_collection_link: u'http://.../~salgado/proposed_members'
705+ recipes_collection_link: u'http://.../~salgado/recipes'
706 resource_type_link: u'http://.../#person'
707 self_link: u'http://.../~salgado'
708 sshkeys_collection_link: u'http://.../~salgado/sshkeys'
709@@ -110,6 +111,7 @@
710 private: False
711 proposed_members_collection_link:
712 u'http://.../~ubuntu-team/proposed_members'
713+ recipes_collection_link: u'http://.../~ubuntu-team/recipes'
714 renewal_policy: u'invite them to apply for renewal'
715 resource_type_link: u'http://.../#team'
716 self_link: u'http://.../~ubuntu-team'
717
718=== modified file 'lib/lp/registry/stories/webservice/xx-project-registry.txt'
719--- lib/lp/registry/stories/webservice/xx-project-registry.txt 2011-02-13 22:54:48 +0000
720+++ lib/lp/registry/stories/webservice/xx-project-registry.txt 2011-02-24 23:45:43 +0000
721@@ -180,6 +180,7 @@
722 programming_language: None
723 project_group_link: u'http://.../mozilla'
724 qualifies_for_free_hosting: False
725+ recipes_collection_link: u'http://.../firefox/recipes'
726 registrant_link: u'http://.../~name12'
727 releases_collection_link: u'http://.../firefox/releases'
728 remote_product: None
729@@ -244,6 +245,7 @@
730 programming_language: None
731 project_group_link: u'http://.../mozilla'
732 qualifies_for_free_hosting: False
733+ recipes_collection_link: u'http://.../firefox/recipes'
734 registrant_link: u'http://.../~name12'
735 releases_collection_link: u'http://.../firefox/releases'
736 remote_product: None
737
738=== modified file 'lib/lp/registry/tests/test_person.py'
739--- lib/lp/registry/tests/test_person.py 2011-02-18 13:26:58 +0000
740+++ lib/lp/registry/tests/test_person.py 2011-02-24 23:45:43 +0000
741@@ -734,7 +734,7 @@
742 self._do_premerge(recipe.owner, person)
743 login_person(person)
744 self.person_set.merge(recipe.owner, person)
745- self.assertEqual(1, person.getRecipes().count())
746+ self.assertEqual(1, person.recipes.count())
747
748 def test_merge_with_duplicated_recipes(self):
749 # If both the from and to people have recipes with the same name,
750@@ -750,7 +750,7 @@
751 self._do_premerge(merge_from.owner, mergee)
752 login_person(mergee)
753 self.person_set.merge(merge_from.owner, merge_to.owner)
754- recipes = mergee.getRecipes()
755+ recipes = mergee.recipes
756 self.assertEqual(2, recipes.count())
757 descriptions = [r.description for r in recipes]
758 self.assertEqual([u'TO', u'FROM'], descriptions)