Merge lp:~bac/launchpad/bug-422128 into lp:launchpad

Proposed by Brad Crittenden
Status: Merged
Merged at revision: not available
Proposed branch: lp:~bac/launchpad/bug-422128
Merge into: lp:launchpad
Diff against target: 689 lines
14 files modified
lib/lp/registry/browser/product.py (+0/-24)
lib/lp/registry/doc/private-team-roles.txt (+53/-5)
lib/lp/registry/doc/product.txt (+69/-1)
lib/lp/registry/interfaces/productrelease.py (+8/-4)
lib/lp/registry/model/milestone.py (+1/-2)
lib/lp/registry/model/product.py (+32/-5)
lib/lp/registry/model/productrelease.py (+6/-3)
lib/lp/registry/stories/webservice/xx-project-registry.txt (+74/-17)
lib/lp/registry/tests/test_doc.py (+12/-0)
lib/lp/services/database/precache.py (+1/-1)
lib/lp/services/database/tests/test_precache.py (+2/-2)
lib/lp/testing/factory.py (+2/-3)
lib/lp/translations/interfaces/translationimportqueue.py (+3/-4)
lib/lp/translations/model/translationimportqueue.py (+4/-3)
To merge this branch: bzr merge lp:~bac/launchpad/bug-422128
Reviewer Review Type Date Requested Status
Paul Hummer (community) code Approve
Canonical Launchpad Engineering Pending
Review via email: mp+12735@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Brad Crittenden (bac) wrote :

= Summary =

Originally it was noted in bug 422128 that changing the ownership of a product to a
private team failed if that product had any product releases owned by the original
owner of the product. That failure was because a ProductRelease could not be owned
by a private team and the view code was changing the ownership of the ProductRelease too.

Further investigation showed that the view code was changing product releases,
product series, and translation import queue entries that were owned by the old
product owner.

== Proposed fix ==

The first fix was to get the ownership reassignment out of the view and into the
model where it belongs. Being in the view meant that changing a product's owner via
the API wouldn't do the same artifact ownership reassignment.

Once that was done, changing ProductRelease and TranslationImportQueueEntry to allow
the owner and importer, respectively, to be a private team was straightforward.

The webservice tests for registry items was moved to the proper place under lp/registry.

== Pre-implementation notes ==

None.

== Implementation details ==

As above.

== Tests ==

bin/test -t private-team-roles.txt -t xx-project-registry.txt -t doc/product.txt

== Demo and Q/A ==

On launchpad.dev change the owner of firefox and ensure it works.

= 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/registry/interfaces/productrelease.py
  lib/lp/registry/model/productrelease.py
  lib/lp/registry/doc/private-team-roles.txt
  lib/lp/registry/doc/product.txt
  lib/lp/registry/browser/product.py
  lib/lp/translations/model/translationimportqueue.py
  lib/lp/testing/factory.py
  lib/lp/registry/tests/test_doc.py
  lib/lp/registry/stories/webservice/xx-project-registry.txt
  lib/lp/registry/model/product.py
  lib/lp/registry/model/milestone.py
  lib/lp/translations/interfaces/translationimportqueue.py

== Pylint notices ==

lib/lp/registry/interfaces/productrelease.py
    25: [F0401] Unable to import 'lazr.enum' (No module named enum)
    34: [F0401] Unable to import 'lazr.restful.fields' (No module named restful)
    35: [F0401] Unable to import 'lazr.restful.interface' (No module named restful)
    36: [F0401] Unable to import 'lazr.restful.declarations' (No module named restful)

lib/lp/registry/browser/product.py
    56: [F0401] Unable to import 'lazr.delegates' (No module named delegates)

lib/lp/registry/model/product.py
    29: [F0401] Unable to import 'lazr.delegates' (No module named delegates)

lib/lp/translations/interfaces/translationimportqueue.py
    9: [F0401] Unable to import 'lazr.enum' (No module named enum)
    19: [F0401] Unable to import 'lazr.restful.interface' (No module named restful)
    20: [F0401] Unable to import 'lazr.restful.fields' (No module named restful)
    21: [F0401] Unable to import 'lazr.restful.declarations' (No module named restful)

Revision history for this message
Paul Hummer (rockstar) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/registry/browser/product.py'
2--- lib/lp/registry/browser/product.py 2009-09-23 14:58:12 +0000
3+++ lib/lp/registry/browser/product.py 2009-10-05 11:36:16 +0000
4@@ -47,7 +47,6 @@
5 from zope.lifecycleevent import ObjectCreatedEvent
6 from zope.interface import implements, Interface
7 from zope.formlib import form
8-from zope.security.proxy import removeSecurityProxy
9
10 from z3c.ptcompat import ViewPageTemplateFile
11
12@@ -65,8 +64,6 @@
13 from lp.services.worlddata.interfaces.country import ICountry
14 from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
15 from canonical.launchpad.interfaces.librarian import ILibraryFileAliasSet
16-from lp.translations.interfaces.translationimportqueue import (
17- ITranslationImportQueue)
18 from canonical.launchpad.webapp.interfaces import (
19 ILaunchBag, NotFoundError, UnsafeFormGetSubmissionError)
20 from lp.registry.interfaces.pillar import IPillarNameSet
21@@ -1709,8 +1706,6 @@
22 old_owner = self.context.owner
23 old_driver = self.context.driver
24 self.updateContextFromData(data)
25- self._reassignProductDependencies(
26- self.context, old_owner, self.context.owner)
27 if self.context.owner != old_owner:
28 self.request.response.addNotification(
29 "Successfully changed the maintainer to %s"
30@@ -1733,22 +1728,3 @@
31 def cancel_url(self):
32 """See `LaunchpadFormView`."""
33 return canonical_url(self.context)
34-
35- def _reassignProductDependencies(self, product, oldOwner, newOwner):
36- """Reassign ownership of objects related to this product.
37-
38- Objects related to this product includes: ProductSeries,
39- ProductReleases and TranslationImportQueueEntries that are owned
40- by oldOwner of the product.
41-
42- """
43- import_queue = getUtility(ITranslationImportQueue)
44- for entry in import_queue.getAllEntries(target=product):
45- if entry.importer == oldOwner:
46- removeSecurityProxy(entry).importer = newOwner
47- for series in product.serieses:
48- if series.owner == oldOwner:
49- series.owner = newOwner
50- for release in product.releases:
51- if release.owner == oldOwner:
52- release.owner = newOwner
53
54=== modified file 'lib/lp/registry/doc/private-team-roles.txt'
55--- lib/lp/registry/doc/private-team-roles.txt 2009-08-11 12:53:54 +0000
56+++ lib/lp/registry/doc/private-team-roles.txt 2009-10-05 11:36:16 +0000
57@@ -332,17 +332,16 @@
58 membership team cannot.
59
60 >>> # The registrant must be specified or it will default to the owner.
61- >>> product = factory.makeProduct(registrant=admin_user, owner=public_team)
62- >>> product = factory.makeProduct(registrant=admin_user, owner=priv_team)
63-
64- >>> product = factory.makeProduct(registrant=admin_user, owner=pm_team)
65+ >>> product = factory.makeProduct(registrant=admin_user)
66+ >>> product.owner = public_team
67+ >>> product.owner = priv_team
68+ >>> product.owner = pm_team
69 Traceback (most recent call last):
70 ...
71 PrivatePersonLinkageError: Cannot link person
72 (name=private-membership-team, visibility=PRIVATE_MEMBERSHIP) to
73 <Product at...
74
75-
76 Driver
77 ------
78
79@@ -419,6 +418,55 @@
80 <ProductSeries at...
81
82
83+Product Release Roles
84+=====================
85+
86+Owner
87+-----
88+
89+A public team and a private team can be a product series owner but a
90+private membership team cannot.
91+
92+ >>> product = factory.makeProduct(registrant=admin_user,
93+ ... owner=public_team)
94+ >>> product_series = factory.makeProductSeries(product, owner=public_team)
95+ >>> product_milestone = factory.makeMilestone(
96+ ... product=product, productseries=product_series)
97+ >>> product_release = factory.makeProductRelease(
98+ ... product=product, milestone=product_milestone)
99+ >>> product_release.owner = public_team
100+ >>> product_release.owner = priv_team
101+ >>> product_release.owner = pm_team
102+ Traceback (most recent call last):
103+ ...
104+ PrivatePersonLinkageError: Cannot link person
105+ (name=private-membership-team, visibility=PRIVATE_MEMBERSHIP) to
106+ <ProductRelease at...
107+
108+Some artifacts of a product change ownership when the product owner
109+changes. The artifacts are product series, product release, and
110+translation import queue entries.
111+
112+ >>> product = factory.makeProduct(registrant=admin_user)
113+ >>> product_series = factory.makeProductSeries(
114+ ... product=product, owner=public_team)
115+ >>> product_release = factory.makeProductRelease(product=product)
116+ >>> from lp.translations.interfaces.translationimportqueue import (
117+ ... ITranslationImportQueue)
118+ >>> import_queue = getUtility(ITranslationImportQueue)
119+ >>> entry = import_queue.addOrUpdateEntry(
120+ ... u'po/sr.po', 'foo', True, public_team,
121+ ... productseries=product_series)
122+ >>> product.owner = public_team
123+ >>> product.owner = priv_team
124+ >>> product.owner = pm_team
125+ Traceback (most recent call last):
126+ ...
127+ PrivatePersonLinkageError: Cannot link person
128+ (name=private-membership-team, visibility=PRIVATE_MEMBERSHIP) to
129+ <Product at...
130+
131+
132 Team Membership
133 ===============
134
135
136=== modified file 'lib/lp/registry/doc/product.txt'
137--- lib/lp/registry/doc/product.txt 2009-07-23 13:44:13 +0000
138+++ lib/lp/registry/doc/product.txt 2009-10-05 11:36:16 +0000
139@@ -63,7 +63,7 @@
140 u'a52dec'
141 >>> productset['a52dec'].name
142 u'a52dec'
143-
144+
145 >>> a52dec.setAliases(['a51dec'])
146 >>> a52dec.aliases
147 [u'a51dec']
148@@ -654,3 +654,71 @@
149 >>> for series in firefox_view.sorted_active_series_list:
150 ... print series.name
151 trunk
152+
153+= Changing ownership =
154+
155+If the owner of a project changes, all series and productreleases
156+owned by the old owner are transfered to the new owner.
157+
158+ >>> print firefox.owner.name
159+ name12
160+
161+ >>> for series in firefox.serieses:
162+ ... print series.owner.name, series.name
163+ name12 1.0
164+ name12 trunk
165+
166+ >>> for release in firefox.releases:
167+ ... print release.owner.name, release.title
168+ name16 Mozilla Firefox 0.9 "One Tree Hill"
169+ name16 Mozilla Firefox 0.9.1 "One Tree Hill (v2)"
170+ name16 Mozilla Firefox 0.9.2 "One (secure) Tree Hill"
171+ name12 Mozilla Firefox 1.0.0 "First Stable Release"
172+
173+ >>> from lp.translations.interfaces.translationimportqueue import (
174+ ... ITranslationImportQueue)
175+ >>> import_queue = getUtility(ITranslationImportQueue)
176+ >>> entry = import_queue.addOrUpdateEntry(
177+ ... u'po/sr.po', 'foo', True, firefox.owner,
178+ ... productseries=firefox.serieses[0])
179+ >>> foobar = getUtility(IPersonSet).getByEmail("foo.bar@canonical.com")
180+ >>> entry = import_queue.addOrUpdateEntry(
181+ ... u'po/sr.po', 'foo', True, foobar,
182+ ... productseries=firefox.serieses[1])
183+ >>> for entry in import_queue.getAllEntries(target=firefox):
184+ ... print entry.importer.name
185+ name12
186+ name16
187+
188+The owner of firefox can be changed.
189+
190+ >>> login("foo.bar@canonical.com")
191+ >>> mark = getUtility(IPersonSet).getByEmail('mark@example.com')
192+ >>> print mark.name
193+ mark
194+
195+ >>> firefox.owner = mark
196+
197+Now that the owner for firefox has changed the series and product
198+releases previously owned by name12 are now owned by mark. Those not
199+owned by name12 are unchanged.
200+
201+ >>> print firefox.owner.name
202+ mark
203+
204+ >>> for series in firefox.serieses:
205+ ... print series.owner.name, series.name
206+ mark 1.0
207+ mark trunk
208+
209+ >>> for release in firefox.releases:
210+ ... print release.owner.name, release.title
211+ name16 Mozilla Firefox 0.9 "One Tree Hill"
212+ name16 Mozilla Firefox 0.9.1 "One Tree Hill (v2)"
213+ name16 Mozilla Firefox 0.9.2 "One (secure) Tree Hill"
214+ mark Mozilla Firefox 1.0.0 "First Stable Release"
215+
216+ >>> for entry in import_queue.getAllEntries(target=firefox):
217+ ... print entry.importer.name
218+ mark
219+ name16
220
221=== modified file 'lib/lp/registry/interfaces/productrelease.py'
222--- lib/lp/registry/interfaces/productrelease.py 2009-06-25 04:06:00 +0000
223+++ lib/lp/registry/interfaces/productrelease.py 2009-10-05 11:36:16 +0000
224@@ -27,8 +27,8 @@
225 from canonical.config import config
226 from canonical.launchpad import _
227 from canonical.launchpad.validators.version import sane_version
228-from canonical.launchpad.fields import ContentNameField
229-from lp.registry.interfaces.person import IPerson
230+from canonical.launchpad.fields import (
231+ ContentNameField, ParticipatingPersonChoice)
232 from canonical.launchpad.validators import LaunchpadValidationError
233
234 from lazr.restful.fields import CollectionField, Reference, ReferenceChoice
235@@ -270,9 +270,13 @@
236 )
237
238 owner = exported(
239- Reference(title=u"The owner of this release.",
240- schema=IPerson, required=True)
241+ ParticipatingPersonChoice(
242+ title=u"The owner of this release.",
243+ required=True,
244+ vocabulary='ValidOwner',
245+ description=_("The person or team who owns his product release.")
246 )
247+ )
248
249 productseries = Choice(
250 title=_('Release series'), readonly=True,
251
252=== modified file 'lib/lp/registry/model/milestone.py'
253--- lib/lp/registry/model/milestone.py 2009-08-24 04:03:27 +0000
254+++ lib/lp/registry/model/milestone.py 2009-10-05 11:36:16 +0000
255@@ -176,7 +176,7 @@
256 """See `IMilestone`."""
257 if self.product_release is not None:
258 raise AssertionError(
259- 'A milestone can only have one Productrelease.')
260+ 'A milestone can only have one ProductRelease.')
261 return ProductRelease(
262 owner=owner,
263 changelog=changelog,
264@@ -301,4 +301,3 @@
265 def official_bug_tags(self):
266 """See `IHasBugs`."""
267 return self.target.official_bug_tags
268-
269
270=== modified file 'lib/lp/registry/model/product.py'
271--- lib/lp/registry/model/product.py 2009-09-16 20:08:36 +0000
272+++ lib/lp/registry/model/product.py 2009-10-05 11:36:16 +0000
273@@ -23,6 +23,7 @@
274 from storm.locals import And, Desc, Join, SQL, Store, Unicode
275 from zope.interface import implements
276 from zope.component import getUtility
277+from zope.security.proxy import removeSecurityProxy
278
279 from canonical.cachedproperty import cachedproperty
280 from lazr.delegates import delegates
281@@ -69,7 +70,7 @@
282 HasSpecificationsMixin, Specification)
283 from lp.blueprints.model.sprint import HasSprintsMixin
284 from lp.translations.model.translationimportqueue import (
285- HasTranslationImportsMixin)
286+ HasTranslationImportsMixin, ITranslationImportQueue)
287 from canonical.launchpad.database.structuralsubscription import (
288 StructuralSubscriptionTargetMixin)
289 from canonical.launchpad.helpers import shortlist
290@@ -177,7 +178,7 @@
291
292 project = ForeignKey(
293 foreignKey="Project", dbName="project", notNull=False, default=None)
294- owner = ForeignKey(
295+ _owner = ForeignKey(
296 dbName="owner", foreignKey="Person",
297 storm_validator=validate_person_not_private_membership,
298 notNull=True)
299@@ -490,6 +491,32 @@
300
301 licenses = property(_getLicenses, _setLicenses)
302
303+ def _getOwner(self):
304+ """Get the owner."""
305+ return self._owner
306+
307+ def _setOwner(self, new_owner):
308+ """Set the owner.
309+
310+ Change the owner and change the ownership of related artifacts.
311+ """
312+ old_owner = self._owner
313+ self._owner = new_owner
314+ if old_owner is not None:
315+ import_queue = getUtility(ITranslationImportQueue)
316+ for entry in import_queue.getAllEntries(target=self):
317+ if entry.importer == old_owner:
318+ removeSecurityProxy(entry).importer = new_owner
319+ for series in self.serieses:
320+ if series.owner == old_owner:
321+ series.owner = new_owner
322+ for release in self.releases:
323+ if release.owner == old_owner:
324+ release.owner = new_owner
325+ Store.of(self).flush()
326+
327+ owner = property(_getOwner, _setOwner)
328+
329 def _getBugTaskContextWhereClause(self):
330 """See BugTargetBase."""
331 return "BugTask.product = %d" % self.id
332@@ -1006,7 +1033,7 @@
333 results = Product.selectBy(
334 active=True, orderBy="-Product.datecreated")
335 # The main product listings include owner, so we prejoin it.
336- return results.prejoin(["owner"])
337+ return results.prejoin(["_owner"])
338
339 def get(self, productid):
340 """See `IProductSet`."""
341@@ -1247,7 +1274,7 @@
342 queries.append('Product.active IS TRUE')
343 query = " AND ".join(queries)
344 return Product.select(query, distinct=True,
345- prejoins=["owner"],
346+ prejoins=["_owner"],
347 clauseTables=clauseTables)
348
349 def getTranslatables(self):
350@@ -1258,7 +1285,7 @@
351 Product.id == ProductSeries.productID,
352 POTemplate.productseriesID == ProductSeries.id,
353 Product.official_rosetta == True,
354- Person.id == Product.ownerID
355+ Person.id == Product._ownerID
356 ).config(distinct=True).order_by(Product.title)
357
358 # We only want Product - the other tables are just to populate
359
360=== modified file 'lib/lp/registry/model/productrelease.py'
361--- lib/lp/registry/model/productrelease.py 2009-07-17 00:26:05 +0000
362+++ lib/lp/registry/model/productrelease.py 2009-10-05 11:36:16 +0000
363@@ -22,9 +22,11 @@
364
365 from canonical.launchpad.webapp.interfaces import NotFoundError
366 from lp.registry.interfaces.productrelease import (
367- IProductRelease, IProductReleaseFile, IProductReleaseSet, UpstreamFileType)
368+ IProductRelease, IProductReleaseFile, IProductReleaseSet,
369+ UpstreamFileType)
370 from canonical.launchpad.interfaces.librarian import ILibraryFileAliasSet
371-from lp.registry.interfaces.person import validate_public_person
372+from lp.registry.interfaces.person import (
373+ validate_person_not_private_membership, validate_public_person)
374 from canonical.launchpad.webapp.interfaces import (
375 DEFAULT_FLAVOR, IStoreSelector, MAIN_STORE)
376
377@@ -45,7 +47,8 @@
378 dbName='datecreated', notNull=True, default=UTC_NOW)
379 owner = ForeignKey(
380 dbName="owner", foreignKey="Person",
381- storm_validator=validate_public_person, notNull=True)
382+ storm_validator=validate_person_not_private_membership,
383+ notNull=True)
384 milestone = ForeignKey(dbName='milestone', foreignKey='Milestone')
385
386 files = SQLMultipleJoin(
387
388=== renamed file 'lib/canonical/launchpad/pagetests/webservice/xx-people.txt' => 'lib/lp/registry/stories/webservice/xx-people.txt'
389=== renamed file 'lib/canonical/launchpad/pagetests/webservice/xx-personlocation.txt' => 'lib/lp/registry/stories/webservice/xx-personlocation.txt'
390=== renamed file 'lib/canonical/launchpad/pagetests/webservice/xx-private-membership.txt' => 'lib/lp/registry/stories/webservice/xx-private-membership.txt'
391=== renamed file 'lib/canonical/launchpad/pagetests/webservice/xx-project-registry.txt' => 'lib/lp/registry/stories/webservice/xx-project-registry.txt'
392--- lib/canonical/launchpad/pagetests/webservice/xx-project-registry.txt 2009-08-21 19:49:18 +0000
393+++ lib/lp/registry/stories/webservice/xx-project-registry.txt 2009-10-05 11:36:16 +0000
394@@ -338,6 +338,47 @@
395 >>> webservice.get(firefox['bug_tracker_link']).jsonBody()['self_link']
396 u'http://.../bugs/bugtrackers/mozilla.org'
397
398+When the owner_link is changed the ownership of some attributes is
399+changed as well.
400+
401+ >>> # Create a product with a series and release.
402+ >>> login('test@canonical.com')
403+ >>> test_project_owner = factory.makePerson(name='test-project-owner')
404+ >>> test_project = factory.makeProduct(name='test-project', owner=test_project_owner)
405+ >>> test_series = factory.makeProductSeries(
406+ ... product=test_project, name='test-series', owner=test_project_owner)
407+ >>> test_milestone = factory.makeMilestone(
408+ ... product=test_project, name='test-milestone', productseries=test_series)
409+ >>> test_project_release = factory.makeProductRelease(
410+ ... product=test_project, milestone=test_milestone)
411+ >>> logout()
412+
413+ >>> test_project = webservice.get('/test-project').jsonBody()
414+ >>> test_project['owner_link']
415+ u'http://.../~test-project-owner'
416+
417+ >>> patch = {
418+ ... u'owner_link': webservice.getAbsoluteUrl('/~mark'),
419+ ... }
420+ >>> print webservice.patch(
421+ ... '/test-project', 'application/json', dumps(patch))
422+ HTTP/1.1 209 Content Returned
423+ ...
424+
425+ >>> test_project = webservice.get('/test-project').jsonBody()
426+ >>> test_project['owner_link']
427+ u'http://.../~mark'
428+
429+ >>> test_series = webservice.get('/test-project/test-series').jsonBody()
430+ >>> test_series['owner_link']
431+ u'http://.../~mark'
432+
433+ >>> release_path = '/test-project/test-series/test-milestone'
434+ >>> test_project_release = webservice.get(release_path).jsonBody()
435+ >>> test_project_release['owner_link']
436+ u'http://.../~mark'
437+
438+
439 Read-only attributes, like registrant, cannot be modified via the
440 webservice.patch() method.
441
442@@ -424,24 +465,37 @@
443 >>> project_collection['resource_type_link']
444 u'http://.../#projects'
445
446+The entire collection has 25 entries.
447+
448 >>> project_collection['total_size']
449- 24
450+ 25
451+
452+But the batch has only 5. (The batch size is 5 for testing but larger
453+in production.)
454+
455+ >>> project_entries = project_collection['entries']
456+ >>> len(project_entries)
457+ 5
458+
459+The batch size can be changed through the ws.size argument.
460+
461+ >>> project_collection = webservice.get("/projects?ws.size=75").jsonBody()
462
463 >>> project_entries = sorted(
464 ... project_collection['entries'], key=itemgetter('display_name'))
465 >>> len(project_entries)
466- 5
467+ 25
468
469 >>> project_entries[0]['self_link']
470- u'http://.../gnome-terminal'
471+ u'http://.../aptoncd'
472
473- >>> for project in project_entries:
474- ... print project['display_name']
475- GNOME Terminal
476- Mozilla Firefox
477- Redfish
478- The Landscape Project
479- Tomcat
480+ >>> for project in project_entries[:5]:
481+ ... print "%s (%s)" % (project['display_name'], project['name'])
482+ APTonCD (aptoncd)
483+ Arch mirrors (arch-mirrors)
484+ Bazaar (bazaar)
485+ Bazaar (bzr)
486+ Derby (derby)
487
488 It's possible to search the list and get a subset of the project groups.
489
490@@ -480,7 +534,7 @@
491 Launchpad Translations
492 Mega Money Maker
493 Obsolete Junk
494- Redfish
495+ Test-project
496
497 There is a method for doing a query about attributes related to project
498 licensing. We can find all projects with unreviewed licenses.
499@@ -726,10 +780,13 @@
500 virtual host.
501
502 >>> login('test@canonical.com')
503- >>> babadoo = factory.makeProduct(name='babadoo')
504- >>> foobadoo = factory.makeProductSeries(product=babadoo, name='foobadoo')
505+ >>> babadoo_owner = factory.makePerson(name='babadoo-owner')
506+ >>> babadoo = factory.makeProduct(name='babadoo', owner=babadoo_owner)
507+ >>> foobadoo = factory.makeProductSeries(
508+ ... product=babadoo, name='foobadoo', owner=babadoo_owner)
509 >>> foobadoo.summary = u'Foobadoo support for Babadoo'
510- >>> fooey = factory.makeAnyBranch(product=babadoo, name='fooey')
511+ >>> fooey = factory.makeAnyBranch(
512+ ... product=babadoo, name='fooey', owner=babadoo_owner)
513 >>> foobadoo.branch = fooey
514 >>> logout()
515
516@@ -737,7 +794,7 @@
517 >>> pprint_entry(babadoo_foobadoo)
518 active_milestones_collection_link: u'http://.../babadoo/foobadoo/active_milestones'
519 all_milestones_collection_link: u'http://.../babadoo/foobadoo/all_milestones'
520- branch_link: u'http://api.launchpad.dev/beta/~person-name12/babadoo/fooey'
521+ branch_link: u'http://.../~babadoo-owner/babadoo/fooey'
522 bug_reporting_guidelines: None
523 date_created: u'...'
524 display_name: u'foobadoo'
525@@ -745,7 +802,7 @@
526 drivers_collection_link: u'http://.../babadoo/foobadoo/drivers'
527 name: u'foobadoo'
528 official_bug_tags: []
529- owner_link: u'http://.../~person-name8'
530+ owner_link: u'http://.../~babadoo-owner'
531 project_link: u'http://.../babadoo'
532 releases_collection_link: u'http://.../babadoo/foobadoo/releases'
533 resource_type_link: u'...'
534@@ -885,7 +942,7 @@
535 >>> print response
536 HTTP/1.1 500 Internal Server Error
537 ...
538- AssertionError: A milestone can only have one Productrelease.
539+ AssertionError: A milestone can only have one ProductRelease.
540
541
542 Project release entries
543
544=== modified file 'lib/lp/registry/tests/test_doc.py'
545--- lib/lp/registry/tests/test_doc.py 2009-07-17 00:26:05 +0000
546+++ lib/lp/registry/tests/test_doc.py 2009-10-05 11:36:16 +0000
547@@ -133,6 +133,18 @@
548 tearDown=tearDown,
549 layer=LaunchpadFunctionalLayer,
550 ),
551+ 'product.txt': LayeredDocFileSuite(
552+ '../doc/product.txt',
553+ setUp=setUp,
554+ tearDown=tearDown,
555+ layer=LaunchpadFunctionalLayer,
556+ ),
557+ 'private-team-roles.txt': LayeredDocFileSuite(
558+ '../doc/private-team-roles.txt',
559+ setUp=setUp,
560+ tearDown=tearDown,
561+ layer=LaunchpadFunctionalLayer,
562+ ),
563 'productrelease.txt': LayeredDocFileSuite(
564 '../doc/productrelease.txt',
565 setUp=setUp,
566
567=== modified file 'lib/lp/services/database/precache.py'
568--- lib/lp/services/database/precache.py 2009-08-05 18:52:52 +0000
569+++ lib/lp/services/database/precache.py 2009-10-05 11:36:16 +0000
570@@ -27,7 +27,7 @@
571
572 >>> results = store.find(Product).precache(
573 ... (Person, EmailAddress),
574- ... Product.ownerID == Person.id,
575+ ... Product._ownerID == Person.id,
576 ... EmailAddress.personID == Person.id)
577 """
578 delegates(IResultSet, context='result_set')
579
580=== modified file 'lib/lp/services/database/tests/test_precache.py'
581--- lib/lp/services/database/tests/test_precache.py 2009-07-17 02:25:09 +0000
582+++ lib/lp/services/database/tests/test_precache.py 2009-10-05 11:36:16 +0000
583@@ -33,7 +33,7 @@
584 # to hide this from callsites.
585 self.unwrapped_result = self.store.find(
586 (Product, Person),
587- Product.ownerID == Person.id).order_by(Product.name)
588+ Product._ownerID == Person.id).order_by(Product.name)
589 self.precache_result = precache(self.unwrapped_result)
590
591 def verify(self, precached, normal):
592@@ -87,7 +87,7 @@
593 standard_result = self.store.find(Product, Product.name == 'firefox')
594 precache_result = precache(self.store.find(
595 (Product, Person),
596- Person.id == Product.ownerID,
597+ Person.id == Product._ownerID,
598 Product.name == 'firefox'))
599 self.assertEqual(standard_result.one(), precache_result.one())
600
601
602=== modified file 'lib/lp/testing/factory.py'
603--- lib/lp/testing/factory.py 2009-09-19 04:06:14 +0000
604+++ lib/lp/testing/factory.py 2009-10-05 11:36:16 +0000
605@@ -76,7 +76,6 @@
606 CodeImportResultStatus, CodeReviewNotificationLevel,
607 RevisionControlSystems)
608 from lp.code.interfaces.branch import UnknownBranchTypeError
609-from lp.code.interfaces.branchtarget import IBranchTarget
610 from lp.code.interfaces.branchmergequeue import IBranchMergeQueueSet
611 from lp.code.interfaces.branchnamespace import get_branch_namespace
612 from lp.code.interfaces.codeimport import ICodeImportSet
613@@ -509,9 +508,9 @@
614 productseries=productseries,
615 name=name)
616
617- def makeProductRelease(self, milestone=None):
618+ def makeProductRelease(self, milestone=None, product=None):
619 if milestone is None:
620- milestone = self.makeMilestone()
621+ milestone = self.makeMilestone(product=product)
622 return milestone.createProductRelease(
623 milestone.product.owner, datetime.now(pytz.UTC))
624
625
626=== modified file 'lib/lp/translations/interfaces/translationimportqueue.py'
627--- lib/lp/translations/interfaces/translationimportqueue.py 2009-09-18 07:39:51 +0000
628+++ lib/lp/translations/interfaces/translationimportqueue.py 2009-10-05 11:36:16 +0000
629@@ -9,15 +9,15 @@
630 from lazr.enum import DBEnumeratedType, DBItem, EnumeratedType, Item
631
632 from canonical.launchpad import _
633+from canonical.launchpad.fields import ParticipatingPersonChoice
634 from lp.registry.interfaces.sourcepackage import ISourcePackage
635 from lp.translations.interfaces.translationfileformat import (
636 TranslationFileFormat)
637 from lp.registry.interfaces.distroseries import IDistroSeries
638-from lp.registry.interfaces.person import IPerson
639 from lp.registry.interfaces.productseries import IProductSeries
640
641 from lazr.restful.interface import copy_field
642-from lazr.restful.fields import Reference, ReferenceChoice
643+from lazr.restful.fields import Reference
644 from lazr.restful.declarations import (
645 collection_default_content, exported, export_as_webservice_collection,
646 export_as_webservice_entry, export_read_operation, operation_parameters,
647@@ -152,9 +152,8 @@
648 required=True))
649
650 importer = exported(
651- ReferenceChoice(
652+ ParticipatingPersonChoice(
653 title=_("Uploader"),
654- schema=IPerson,
655 required=True,
656 readonly=True,
657 description=_(
658
659=== modified file 'lib/lp/translations/model/translationimportqueue.py'
660--- lib/lp/translations/model/translationimportqueue.py 2009-09-28 09:46:27 +0000
661+++ lib/lp/translations/model/translationimportqueue.py 2009-10-05 11:36:16 +0000
662@@ -38,7 +38,8 @@
663 from lp.registry.interfaces.distribution import IDistribution
664 from lp.registry.interfaces.distroseries import (
665 IDistroSeries, DistroSeriesStatus)
666-from lp.registry.interfaces.person import IPerson
667+from lp.registry.interfaces.person import (
668+ IPerson, validate_person_not_private_membership)
669 from lp.registry.interfaces.product import IProduct
670 from lp.registry.interfaces.productseries import IProductSeries
671 from lp.registry.interfaces.sourcepackage import ISourcePackage
672@@ -61,7 +62,6 @@
673 from lp.translations.utilities.gettext_po_importer import (
674 GettextPOImporter)
675 from canonical.librarian.interfaces import ILibrarianClient
676-from lp.registry.interfaces.person import validate_public_person
677
678
679 # Period to wait before entries with terminal statuses are removed from
680@@ -120,7 +120,8 @@
681 notNull=False)
682 importer = ForeignKey(
683 dbName='importer', foreignKey='Person',
684- storm_validator=validate_public_person, notNull=True)
685+ storm_validator=validate_person_not_private_membership,
686+ notNull=True)
687 dateimported = UtcDateTimeCol(dbName='dateimported', notNull=True,
688 default=DEFAULT)
689 sourcepackagename_id = Int(name='sourcepackagename', allow_none=True)