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
=== modified file 'lib/lp/registry/browser/product.py'
--- lib/lp/registry/browser/product.py 2009-09-23 14:58:12 +0000
+++ lib/lp/registry/browser/product.py 2009-10-05 11:36:16 +0000
@@ -47,7 +47,6 @@
47from zope.lifecycleevent import ObjectCreatedEvent47from zope.lifecycleevent import ObjectCreatedEvent
48from zope.interface import implements, Interface48from zope.interface import implements, Interface
49from zope.formlib import form49from zope.formlib import form
50from zope.security.proxy import removeSecurityProxy
5150
52from z3c.ptcompat import ViewPageTemplateFile51from z3c.ptcompat import ViewPageTemplateFile
5352
@@ -65,8 +64,6 @@
65from lp.services.worlddata.interfaces.country import ICountry64from lp.services.worlddata.interfaces.country import ICountry
66from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities65from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
67from canonical.launchpad.interfaces.librarian import ILibraryFileAliasSet66from canonical.launchpad.interfaces.librarian import ILibraryFileAliasSet
68from lp.translations.interfaces.translationimportqueue import (
69 ITranslationImportQueue)
70from canonical.launchpad.webapp.interfaces import (67from canonical.launchpad.webapp.interfaces import (
71 ILaunchBag, NotFoundError, UnsafeFormGetSubmissionError)68 ILaunchBag, NotFoundError, UnsafeFormGetSubmissionError)
72from lp.registry.interfaces.pillar import IPillarNameSet69from lp.registry.interfaces.pillar import IPillarNameSet
@@ -1709,8 +1706,6 @@
1709 old_owner = self.context.owner1706 old_owner = self.context.owner
1710 old_driver = self.context.driver1707 old_driver = self.context.driver
1711 self.updateContextFromData(data)1708 self.updateContextFromData(data)
1712 self._reassignProductDependencies(
1713 self.context, old_owner, self.context.owner)
1714 if self.context.owner != old_owner:1709 if self.context.owner != old_owner:
1715 self.request.response.addNotification(1710 self.request.response.addNotification(
1716 "Successfully changed the maintainer to %s"1711 "Successfully changed the maintainer to %s"
@@ -1733,22 +1728,3 @@
1733 def cancel_url(self):1728 def cancel_url(self):
1734 """See `LaunchpadFormView`."""1729 """See `LaunchpadFormView`."""
1735 return canonical_url(self.context)1730 return canonical_url(self.context)
1736
1737 def _reassignProductDependencies(self, product, oldOwner, newOwner):
1738 """Reassign ownership of objects related to this product.
1739
1740 Objects related to this product includes: ProductSeries,
1741 ProductReleases and TranslationImportQueueEntries that are owned
1742 by oldOwner of the product.
1743
1744 """
1745 import_queue = getUtility(ITranslationImportQueue)
1746 for entry in import_queue.getAllEntries(target=product):
1747 if entry.importer == oldOwner:
1748 removeSecurityProxy(entry).importer = newOwner
1749 for series in product.serieses:
1750 if series.owner == oldOwner:
1751 series.owner = newOwner
1752 for release in product.releases:
1753 if release.owner == oldOwner:
1754 release.owner = newOwner
17551731
=== modified file 'lib/lp/registry/doc/private-team-roles.txt'
--- lib/lp/registry/doc/private-team-roles.txt 2009-08-11 12:53:54 +0000
+++ lib/lp/registry/doc/private-team-roles.txt 2009-10-05 11:36:16 +0000
@@ -332,17 +332,16 @@
332membership team cannot.332membership team cannot.
333333
334 >>> # The registrant must be specified or it will default to the owner.334 >>> # The registrant must be specified or it will default to the owner.
335 >>> product = factory.makeProduct(registrant=admin_user, owner=public_team)335 >>> product = factory.makeProduct(registrant=admin_user)
336 >>> product = factory.makeProduct(registrant=admin_user, owner=priv_team)336 >>> product.owner = public_team
337337 >>> product.owner = priv_team
338 >>> product = factory.makeProduct(registrant=admin_user, owner=pm_team)338 >>> product.owner = pm_team
339 Traceback (most recent call last):339 Traceback (most recent call last):
340 ...340 ...
341 PrivatePersonLinkageError: Cannot link person341 PrivatePersonLinkageError: Cannot link person
342 (name=private-membership-team, visibility=PRIVATE_MEMBERSHIP) to342 (name=private-membership-team, visibility=PRIVATE_MEMBERSHIP) to
343 <Product at...343 <Product at...
344344
345
346Driver345Driver
347------346------
348347
@@ -419,6 +418,55 @@
419 <ProductSeries at...418 <ProductSeries at...
420419
421420
421Product Release Roles
422=====================
423
424Owner
425-----
426
427A public team and a private team can be a product series owner but a
428private membership team cannot.
429
430 >>> product = factory.makeProduct(registrant=admin_user,
431 ... owner=public_team)
432 >>> product_series = factory.makeProductSeries(product, owner=public_team)
433 >>> product_milestone = factory.makeMilestone(
434 ... product=product, productseries=product_series)
435 >>> product_release = factory.makeProductRelease(
436 ... product=product, milestone=product_milestone)
437 >>> product_release.owner = public_team
438 >>> product_release.owner = priv_team
439 >>> product_release.owner = pm_team
440 Traceback (most recent call last):
441 ...
442 PrivatePersonLinkageError: Cannot link person
443 (name=private-membership-team, visibility=PRIVATE_MEMBERSHIP) to
444 <ProductRelease at...
445
446Some artifacts of a product change ownership when the product owner
447changes. The artifacts are product series, product release, and
448translation import queue entries.
449
450 >>> product = factory.makeProduct(registrant=admin_user)
451 >>> product_series = factory.makeProductSeries(
452 ... product=product, owner=public_team)
453 >>> product_release = factory.makeProductRelease(product=product)
454 >>> from lp.translations.interfaces.translationimportqueue import (
455 ... ITranslationImportQueue)
456 >>> import_queue = getUtility(ITranslationImportQueue)
457 >>> entry = import_queue.addOrUpdateEntry(
458 ... u'po/sr.po', 'foo', True, public_team,
459 ... productseries=product_series)
460 >>> product.owner = public_team
461 >>> product.owner = priv_team
462 >>> product.owner = pm_team
463 Traceback (most recent call last):
464 ...
465 PrivatePersonLinkageError: Cannot link person
466 (name=private-membership-team, visibility=PRIVATE_MEMBERSHIP) to
467 <Product at...
468
469
422Team Membership470Team Membership
423===============471===============
424472
425473
=== modified file 'lib/lp/registry/doc/product.txt'
--- lib/lp/registry/doc/product.txt 2009-07-23 13:44:13 +0000
+++ lib/lp/registry/doc/product.txt 2009-10-05 11:36:16 +0000
@@ -63,7 +63,7 @@
63 u'a52dec'63 u'a52dec'
64 >>> productset['a52dec'].name64 >>> productset['a52dec'].name
65 u'a52dec'65 u'a52dec'
66 66
67 >>> a52dec.setAliases(['a51dec'])67 >>> a52dec.setAliases(['a51dec'])
68 >>> a52dec.aliases68 >>> a52dec.aliases
69 [u'a51dec']69 [u'a51dec']
@@ -654,3 +654,71 @@
654 >>> for series in firefox_view.sorted_active_series_list:654 >>> for series in firefox_view.sorted_active_series_list:
655 ... print series.name655 ... print series.name
656 trunk656 trunk
657
658= Changing ownership =
659
660If the owner of a project changes, all series and productreleases
661owned by the old owner are transfered to the new owner.
662
663 >>> print firefox.owner.name
664 name12
665
666 >>> for series in firefox.serieses:
667 ... print series.owner.name, series.name
668 name12 1.0
669 name12 trunk
670
671 >>> for release in firefox.releases:
672 ... print release.owner.name, release.title
673 name16 Mozilla Firefox 0.9 "One Tree Hill"
674 name16 Mozilla Firefox 0.9.1 "One Tree Hill (v2)"
675 name16 Mozilla Firefox 0.9.2 "One (secure) Tree Hill"
676 name12 Mozilla Firefox 1.0.0 "First Stable Release"
677
678 >>> from lp.translations.interfaces.translationimportqueue import (
679 ... ITranslationImportQueue)
680 >>> import_queue = getUtility(ITranslationImportQueue)
681 >>> entry = import_queue.addOrUpdateEntry(
682 ... u'po/sr.po', 'foo', True, firefox.owner,
683 ... productseries=firefox.serieses[0])
684 >>> foobar = getUtility(IPersonSet).getByEmail("foo.bar@canonical.com")
685 >>> entry = import_queue.addOrUpdateEntry(
686 ... u'po/sr.po', 'foo', True, foobar,
687 ... productseries=firefox.serieses[1])
688 >>> for entry in import_queue.getAllEntries(target=firefox):
689 ... print entry.importer.name
690 name12
691 name16
692
693The owner of firefox can be changed.
694
695 >>> login("foo.bar@canonical.com")
696 >>> mark = getUtility(IPersonSet).getByEmail('mark@example.com')
697 >>> print mark.name
698 mark
699
700 >>> firefox.owner = mark
701
702Now that the owner for firefox has changed the series and product
703releases previously owned by name12 are now owned by mark. Those not
704owned by name12 are unchanged.
705
706 >>> print firefox.owner.name
707 mark
708
709 >>> for series in firefox.serieses:
710 ... print series.owner.name, series.name
711 mark 1.0
712 mark trunk
713
714 >>> for release in firefox.releases:
715 ... print release.owner.name, release.title
716 name16 Mozilla Firefox 0.9 "One Tree Hill"
717 name16 Mozilla Firefox 0.9.1 "One Tree Hill (v2)"
718 name16 Mozilla Firefox 0.9.2 "One (secure) Tree Hill"
719 mark Mozilla Firefox 1.0.0 "First Stable Release"
720
721 >>> for entry in import_queue.getAllEntries(target=firefox):
722 ... print entry.importer.name
723 mark
724 name16
657725
=== modified file 'lib/lp/registry/interfaces/productrelease.py'
--- lib/lp/registry/interfaces/productrelease.py 2009-06-25 04:06:00 +0000
+++ lib/lp/registry/interfaces/productrelease.py 2009-10-05 11:36:16 +0000
@@ -27,8 +27,8 @@
27from canonical.config import config27from canonical.config import config
28from canonical.launchpad import _28from canonical.launchpad import _
29from canonical.launchpad.validators.version import sane_version29from canonical.launchpad.validators.version import sane_version
30from canonical.launchpad.fields import ContentNameField30from canonical.launchpad.fields import (
31from lp.registry.interfaces.person import IPerson31 ContentNameField, ParticipatingPersonChoice)
32from canonical.launchpad.validators import LaunchpadValidationError32from canonical.launchpad.validators import LaunchpadValidationError
3333
34from lazr.restful.fields import CollectionField, Reference, ReferenceChoice34from lazr.restful.fields import CollectionField, Reference, ReferenceChoice
@@ -270,9 +270,13 @@
270 )270 )
271271
272 owner = exported(272 owner = exported(
273 Reference(title=u"The owner of this release.",273 ParticipatingPersonChoice(
274 schema=IPerson, required=True)274 title=u"The owner of this release.",
275 required=True,
276 vocabulary='ValidOwner',
277 description=_("The person or team who owns his product release.")
275 )278 )
279 )
276280
277 productseries = Choice(281 productseries = Choice(
278 title=_('Release series'), readonly=True,282 title=_('Release series'), readonly=True,
279283
=== modified file 'lib/lp/registry/model/milestone.py'
--- lib/lp/registry/model/milestone.py 2009-08-24 04:03:27 +0000
+++ lib/lp/registry/model/milestone.py 2009-10-05 11:36:16 +0000
@@ -176,7 +176,7 @@
176 """See `IMilestone`."""176 """See `IMilestone`."""
177 if self.product_release is not None:177 if self.product_release is not None:
178 raise AssertionError(178 raise AssertionError(
179 'A milestone can only have one Productrelease.')179 'A milestone can only have one ProductRelease.')
180 return ProductRelease(180 return ProductRelease(
181 owner=owner,181 owner=owner,
182 changelog=changelog,182 changelog=changelog,
@@ -301,4 +301,3 @@
301 def official_bug_tags(self):301 def official_bug_tags(self):
302 """See `IHasBugs`."""302 """See `IHasBugs`."""
303 return self.target.official_bug_tags303 return self.target.official_bug_tags
304
305304
=== modified file 'lib/lp/registry/model/product.py'
--- lib/lp/registry/model/product.py 2009-09-16 20:08:36 +0000
+++ lib/lp/registry/model/product.py 2009-10-05 11:36:16 +0000
@@ -23,6 +23,7 @@
23from storm.locals import And, Desc, Join, SQL, Store, Unicode23from storm.locals import And, Desc, Join, SQL, Store, Unicode
24from zope.interface import implements24from zope.interface import implements
25from zope.component import getUtility25from zope.component import getUtility
26from zope.security.proxy import removeSecurityProxy
2627
27from canonical.cachedproperty import cachedproperty28from canonical.cachedproperty import cachedproperty
28from lazr.delegates import delegates29from lazr.delegates import delegates
@@ -69,7 +70,7 @@
69 HasSpecificationsMixin, Specification)70 HasSpecificationsMixin, Specification)
70from lp.blueprints.model.sprint import HasSprintsMixin71from lp.blueprints.model.sprint import HasSprintsMixin
71from lp.translations.model.translationimportqueue import (72from lp.translations.model.translationimportqueue import (
72 HasTranslationImportsMixin)73 HasTranslationImportsMixin, ITranslationImportQueue)
73from canonical.launchpad.database.structuralsubscription import (74from canonical.launchpad.database.structuralsubscription import (
74 StructuralSubscriptionTargetMixin)75 StructuralSubscriptionTargetMixin)
75from canonical.launchpad.helpers import shortlist76from canonical.launchpad.helpers import shortlist
@@ -177,7 +178,7 @@
177178
178 project = ForeignKey(179 project = ForeignKey(
179 foreignKey="Project", dbName="project", notNull=False, default=None)180 foreignKey="Project", dbName="project", notNull=False, default=None)
180 owner = ForeignKey(181 _owner = ForeignKey(
181 dbName="owner", foreignKey="Person",182 dbName="owner", foreignKey="Person",
182 storm_validator=validate_person_not_private_membership,183 storm_validator=validate_person_not_private_membership,
183 notNull=True)184 notNull=True)
@@ -490,6 +491,32 @@
490491
491 licenses = property(_getLicenses, _setLicenses)492 licenses = property(_getLicenses, _setLicenses)
492493
494 def _getOwner(self):
495 """Get the owner."""
496 return self._owner
497
498 def _setOwner(self, new_owner):
499 """Set the owner.
500
501 Change the owner and change the ownership of related artifacts.
502 """
503 old_owner = self._owner
504 self._owner = new_owner
505 if old_owner is not None:
506 import_queue = getUtility(ITranslationImportQueue)
507 for entry in import_queue.getAllEntries(target=self):
508 if entry.importer == old_owner:
509 removeSecurityProxy(entry).importer = new_owner
510 for series in self.serieses:
511 if series.owner == old_owner:
512 series.owner = new_owner
513 for release in self.releases:
514 if release.owner == old_owner:
515 release.owner = new_owner
516 Store.of(self).flush()
517
518 owner = property(_getOwner, _setOwner)
519
493 def _getBugTaskContextWhereClause(self):520 def _getBugTaskContextWhereClause(self):
494 """See BugTargetBase."""521 """See BugTargetBase."""
495 return "BugTask.product = %d" % self.id522 return "BugTask.product = %d" % self.id
@@ -1006,7 +1033,7 @@
1006 results = Product.selectBy(1033 results = Product.selectBy(
1007 active=True, orderBy="-Product.datecreated")1034 active=True, orderBy="-Product.datecreated")
1008 # The main product listings include owner, so we prejoin it.1035 # The main product listings include owner, so we prejoin it.
1009 return results.prejoin(["owner"])1036 return results.prejoin(["_owner"])
10101037
1011 def get(self, productid):1038 def get(self, productid):
1012 """See `IProductSet`."""1039 """See `IProductSet`."""
@@ -1247,7 +1274,7 @@
1247 queries.append('Product.active IS TRUE')1274 queries.append('Product.active IS TRUE')
1248 query = " AND ".join(queries)1275 query = " AND ".join(queries)
1249 return Product.select(query, distinct=True,1276 return Product.select(query, distinct=True,
1250 prejoins=["owner"],1277 prejoins=["_owner"],
1251 clauseTables=clauseTables)1278 clauseTables=clauseTables)
12521279
1253 def getTranslatables(self):1280 def getTranslatables(self):
@@ -1258,7 +1285,7 @@
1258 Product.id == ProductSeries.productID,1285 Product.id == ProductSeries.productID,
1259 POTemplate.productseriesID == ProductSeries.id,1286 POTemplate.productseriesID == ProductSeries.id,
1260 Product.official_rosetta == True,1287 Product.official_rosetta == True,
1261 Person.id == Product.ownerID1288 Person.id == Product._ownerID
1262 ).config(distinct=True).order_by(Product.title)1289 ).config(distinct=True).order_by(Product.title)
12631290
1264 # We only want Product - the other tables are just to populate1291 # We only want Product - the other tables are just to populate
12651292
=== modified file 'lib/lp/registry/model/productrelease.py'
--- lib/lp/registry/model/productrelease.py 2009-07-17 00:26:05 +0000
+++ lib/lp/registry/model/productrelease.py 2009-10-05 11:36:16 +0000
@@ -22,9 +22,11 @@
2222
23from canonical.launchpad.webapp.interfaces import NotFoundError23from canonical.launchpad.webapp.interfaces import NotFoundError
24from lp.registry.interfaces.productrelease import (24from lp.registry.interfaces.productrelease import (
25 IProductRelease, IProductReleaseFile, IProductReleaseSet, UpstreamFileType)25 IProductRelease, IProductReleaseFile, IProductReleaseSet,
26 UpstreamFileType)
26from canonical.launchpad.interfaces.librarian import ILibraryFileAliasSet27from canonical.launchpad.interfaces.librarian import ILibraryFileAliasSet
27from lp.registry.interfaces.person import validate_public_person28from lp.registry.interfaces.person import (
29 validate_person_not_private_membership, validate_public_person)
28from canonical.launchpad.webapp.interfaces import (30from canonical.launchpad.webapp.interfaces import (
29 DEFAULT_FLAVOR, IStoreSelector, MAIN_STORE)31 DEFAULT_FLAVOR, IStoreSelector, MAIN_STORE)
3032
@@ -45,7 +47,8 @@
45 dbName='datecreated', notNull=True, default=UTC_NOW)47 dbName='datecreated', notNull=True, default=UTC_NOW)
46 owner = ForeignKey(48 owner = ForeignKey(
47 dbName="owner", foreignKey="Person",49 dbName="owner", foreignKey="Person",
48 storm_validator=validate_public_person, notNull=True)50 storm_validator=validate_person_not_private_membership,
51 notNull=True)
49 milestone = ForeignKey(dbName='milestone', foreignKey='Milestone')52 milestone = ForeignKey(dbName='milestone', foreignKey='Milestone')
5053
51 files = SQLMultipleJoin(54 files = SQLMultipleJoin(
5255
=== renamed file 'lib/canonical/launchpad/pagetests/webservice/xx-people.txt' => 'lib/lp/registry/stories/webservice/xx-people.txt'
=== renamed file 'lib/canonical/launchpad/pagetests/webservice/xx-personlocation.txt' => 'lib/lp/registry/stories/webservice/xx-personlocation.txt'
=== renamed file 'lib/canonical/launchpad/pagetests/webservice/xx-private-membership.txt' => 'lib/lp/registry/stories/webservice/xx-private-membership.txt'
=== renamed file 'lib/canonical/launchpad/pagetests/webservice/xx-project-registry.txt' => 'lib/lp/registry/stories/webservice/xx-project-registry.txt'
--- lib/canonical/launchpad/pagetests/webservice/xx-project-registry.txt 2009-08-21 19:49:18 +0000
+++ lib/lp/registry/stories/webservice/xx-project-registry.txt 2009-10-05 11:36:16 +0000
@@ -338,6 +338,47 @@
338 >>> webservice.get(firefox['bug_tracker_link']).jsonBody()['self_link']338 >>> webservice.get(firefox['bug_tracker_link']).jsonBody()['self_link']
339 u'http://.../bugs/bugtrackers/mozilla.org'339 u'http://.../bugs/bugtrackers/mozilla.org'
340340
341When the owner_link is changed the ownership of some attributes is
342changed as well.
343
344 >>> # Create a product with a series and release.
345 >>> login('test@canonical.com')
346 >>> test_project_owner = factory.makePerson(name='test-project-owner')
347 >>> test_project = factory.makeProduct(name='test-project', owner=test_project_owner)
348 >>> test_series = factory.makeProductSeries(
349 ... product=test_project, name='test-series', owner=test_project_owner)
350 >>> test_milestone = factory.makeMilestone(
351 ... product=test_project, name='test-milestone', productseries=test_series)
352 >>> test_project_release = factory.makeProductRelease(
353 ... product=test_project, milestone=test_milestone)
354 >>> logout()
355
356 >>> test_project = webservice.get('/test-project').jsonBody()
357 >>> test_project['owner_link']
358 u'http://.../~test-project-owner'
359
360 >>> patch = {
361 ... u'owner_link': webservice.getAbsoluteUrl('/~mark'),
362 ... }
363 >>> print webservice.patch(
364 ... '/test-project', 'application/json', dumps(patch))
365 HTTP/1.1 209 Content Returned
366 ...
367
368 >>> test_project = webservice.get('/test-project').jsonBody()
369 >>> test_project['owner_link']
370 u'http://.../~mark'
371
372 >>> test_series = webservice.get('/test-project/test-series').jsonBody()
373 >>> test_series['owner_link']
374 u'http://.../~mark'
375
376 >>> release_path = '/test-project/test-series/test-milestone'
377 >>> test_project_release = webservice.get(release_path).jsonBody()
378 >>> test_project_release['owner_link']
379 u'http://.../~mark'
380
381
341Read-only attributes, like registrant, cannot be modified via the382Read-only attributes, like registrant, cannot be modified via the
342webservice.patch() method.383webservice.patch() method.
343384
@@ -424,24 +465,37 @@
424 >>> project_collection['resource_type_link']465 >>> project_collection['resource_type_link']
425 u'http://.../#projects'466 u'http://.../#projects'
426467
468The entire collection has 25 entries.
469
427 >>> project_collection['total_size']470 >>> project_collection['total_size']
428 24471 25
472
473But the batch has only 5. (The batch size is 5 for testing but larger
474in production.)
475
476 >>> project_entries = project_collection['entries']
477 >>> len(project_entries)
478 5
479
480The batch size can be changed through the ws.size argument.
481
482 >>> project_collection = webservice.get("/projects?ws.size=75").jsonBody()
429483
430 >>> project_entries = sorted(484 >>> project_entries = sorted(
431 ... project_collection['entries'], key=itemgetter('display_name'))485 ... project_collection['entries'], key=itemgetter('display_name'))
432 >>> len(project_entries)486 >>> len(project_entries)
433 5487 25
434488
435 >>> project_entries[0]['self_link']489 >>> project_entries[0]['self_link']
436 u'http://.../gnome-terminal'490 u'http://.../aptoncd'
437491
438 >>> for project in project_entries:492 >>> for project in project_entries[:5]:
439 ... print project['display_name']493 ... print "%s (%s)" % (project['display_name'], project['name'])
440 GNOME Terminal494 APTonCD (aptoncd)
441 Mozilla Firefox495 Arch mirrors (arch-mirrors)
442 Redfish496 Bazaar (bazaar)
443 The Landscape Project497 Bazaar (bzr)
444 Tomcat498 Derby (derby)
445499
446It's possible to search the list and get a subset of the project groups.500It's possible to search the list and get a subset of the project groups.
447501
@@ -480,7 +534,7 @@
480 Launchpad Translations534 Launchpad Translations
481 Mega Money Maker535 Mega Money Maker
482 Obsolete Junk536 Obsolete Junk
483 Redfish537 Test-project
484538
485There is a method for doing a query about attributes related to project539There is a method for doing a query about attributes related to project
486licensing. We can find all projects with unreviewed licenses.540licensing. We can find all projects with unreviewed licenses.
@@ -726,10 +780,13 @@
726virtual host.780virtual host.
727781
728 >>> login('test@canonical.com')782 >>> login('test@canonical.com')
729 >>> babadoo = factory.makeProduct(name='babadoo')783 >>> babadoo_owner = factory.makePerson(name='babadoo-owner')
730 >>> foobadoo = factory.makeProductSeries(product=babadoo, name='foobadoo')784 >>> babadoo = factory.makeProduct(name='babadoo', owner=babadoo_owner)
785 >>> foobadoo = factory.makeProductSeries(
786 ... product=babadoo, name='foobadoo', owner=babadoo_owner)
731 >>> foobadoo.summary = u'Foobadoo support for Babadoo'787 >>> foobadoo.summary = u'Foobadoo support for Babadoo'
732 >>> fooey = factory.makeAnyBranch(product=babadoo, name='fooey')788 >>> fooey = factory.makeAnyBranch(
789 ... product=babadoo, name='fooey', owner=babadoo_owner)
733 >>> foobadoo.branch = fooey790 >>> foobadoo.branch = fooey
734 >>> logout()791 >>> logout()
735792
@@ -737,7 +794,7 @@
737 >>> pprint_entry(babadoo_foobadoo)794 >>> pprint_entry(babadoo_foobadoo)
738 active_milestones_collection_link: u'http://.../babadoo/foobadoo/active_milestones'795 active_milestones_collection_link: u'http://.../babadoo/foobadoo/active_milestones'
739 all_milestones_collection_link: u'http://.../babadoo/foobadoo/all_milestones'796 all_milestones_collection_link: u'http://.../babadoo/foobadoo/all_milestones'
740 branch_link: u'http://api.launchpad.dev/beta/~person-name12/babadoo/fooey'797 branch_link: u'http://.../~babadoo-owner/babadoo/fooey'
741 bug_reporting_guidelines: None798 bug_reporting_guidelines: None
742 date_created: u'...'799 date_created: u'...'
743 display_name: u'foobadoo'800 display_name: u'foobadoo'
@@ -745,7 +802,7 @@
745 drivers_collection_link: u'http://.../babadoo/foobadoo/drivers'802 drivers_collection_link: u'http://.../babadoo/foobadoo/drivers'
746 name: u'foobadoo'803 name: u'foobadoo'
747 official_bug_tags: []804 official_bug_tags: []
748 owner_link: u'http://.../~person-name8'805 owner_link: u'http://.../~babadoo-owner'
749 project_link: u'http://.../babadoo'806 project_link: u'http://.../babadoo'
750 releases_collection_link: u'http://.../babadoo/foobadoo/releases'807 releases_collection_link: u'http://.../babadoo/foobadoo/releases'
751 resource_type_link: u'...'808 resource_type_link: u'...'
@@ -885,7 +942,7 @@
885 >>> print response942 >>> print response
886 HTTP/1.1 500 Internal Server Error943 HTTP/1.1 500 Internal Server Error
887 ...944 ...
888 AssertionError: A milestone can only have one Productrelease.945 AssertionError: A milestone can only have one ProductRelease.
889946
890947
891Project release entries948Project release entries
892949
=== modified file 'lib/lp/registry/tests/test_doc.py'
--- lib/lp/registry/tests/test_doc.py 2009-07-17 00:26:05 +0000
+++ lib/lp/registry/tests/test_doc.py 2009-10-05 11:36:16 +0000
@@ -133,6 +133,18 @@
133 tearDown=tearDown,133 tearDown=tearDown,
134 layer=LaunchpadFunctionalLayer,134 layer=LaunchpadFunctionalLayer,
135 ),135 ),
136 'product.txt': LayeredDocFileSuite(
137 '../doc/product.txt',
138 setUp=setUp,
139 tearDown=tearDown,
140 layer=LaunchpadFunctionalLayer,
141 ),
142 'private-team-roles.txt': LayeredDocFileSuite(
143 '../doc/private-team-roles.txt',
144 setUp=setUp,
145 tearDown=tearDown,
146 layer=LaunchpadFunctionalLayer,
147 ),
136 'productrelease.txt': LayeredDocFileSuite(148 'productrelease.txt': LayeredDocFileSuite(
137 '../doc/productrelease.txt',149 '../doc/productrelease.txt',
138 setUp=setUp,150 setUp=setUp,
139151
=== modified file 'lib/lp/services/database/precache.py'
--- lib/lp/services/database/precache.py 2009-08-05 18:52:52 +0000
+++ lib/lp/services/database/precache.py 2009-10-05 11:36:16 +0000
@@ -27,7 +27,7 @@
2727
28 >>> results = store.find(Product).precache(28 >>> results = store.find(Product).precache(
29 ... (Person, EmailAddress),29 ... (Person, EmailAddress),
30 ... Product.ownerID == Person.id,30 ... Product._ownerID == Person.id,
31 ... EmailAddress.personID == Person.id)31 ... EmailAddress.personID == Person.id)
32 """32 """
33 delegates(IResultSet, context='result_set')33 delegates(IResultSet, context='result_set')
3434
=== modified file 'lib/lp/services/database/tests/test_precache.py'
--- lib/lp/services/database/tests/test_precache.py 2009-07-17 02:25:09 +0000
+++ lib/lp/services/database/tests/test_precache.py 2009-10-05 11:36:16 +0000
@@ -33,7 +33,7 @@
33 # to hide this from callsites.33 # to hide this from callsites.
34 self.unwrapped_result = self.store.find(34 self.unwrapped_result = self.store.find(
35 (Product, Person),35 (Product, Person),
36 Product.ownerID == Person.id).order_by(Product.name)36 Product._ownerID == Person.id).order_by(Product.name)
37 self.precache_result = precache(self.unwrapped_result)37 self.precache_result = precache(self.unwrapped_result)
3838
39 def verify(self, precached, normal):39 def verify(self, precached, normal):
@@ -87,7 +87,7 @@
87 standard_result = self.store.find(Product, Product.name == 'firefox')87 standard_result = self.store.find(Product, Product.name == 'firefox')
88 precache_result = precache(self.store.find(88 precache_result = precache(self.store.find(
89 (Product, Person),89 (Product, Person),
90 Person.id == Product.ownerID,90 Person.id == Product._ownerID,
91 Product.name == 'firefox'))91 Product.name == 'firefox'))
92 self.assertEqual(standard_result.one(), precache_result.one())92 self.assertEqual(standard_result.one(), precache_result.one())
9393
9494
=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py 2009-09-19 04:06:14 +0000
+++ lib/lp/testing/factory.py 2009-10-05 11:36:16 +0000
@@ -76,7 +76,6 @@
76 CodeImportResultStatus, CodeReviewNotificationLevel,76 CodeImportResultStatus, CodeReviewNotificationLevel,
77 RevisionControlSystems)77 RevisionControlSystems)
78from lp.code.interfaces.branch import UnknownBranchTypeError78from lp.code.interfaces.branch import UnknownBranchTypeError
79from lp.code.interfaces.branchtarget import IBranchTarget
80from lp.code.interfaces.branchmergequeue import IBranchMergeQueueSet79from lp.code.interfaces.branchmergequeue import IBranchMergeQueueSet
81from lp.code.interfaces.branchnamespace import get_branch_namespace80from lp.code.interfaces.branchnamespace import get_branch_namespace
82from lp.code.interfaces.codeimport import ICodeImportSet81from lp.code.interfaces.codeimport import ICodeImportSet
@@ -509,9 +508,9 @@
509 productseries=productseries,508 productseries=productseries,
510 name=name)509 name=name)
511510
512 def makeProductRelease(self, milestone=None):511 def makeProductRelease(self, milestone=None, product=None):
513 if milestone is None:512 if milestone is None:
514 milestone = self.makeMilestone()513 milestone = self.makeMilestone(product=product)
515 return milestone.createProductRelease(514 return milestone.createProductRelease(
516 milestone.product.owner, datetime.now(pytz.UTC))515 milestone.product.owner, datetime.now(pytz.UTC))
517516
518517
=== modified file 'lib/lp/translations/interfaces/translationimportqueue.py'
--- lib/lp/translations/interfaces/translationimportqueue.py 2009-09-18 07:39:51 +0000
+++ lib/lp/translations/interfaces/translationimportqueue.py 2009-10-05 11:36:16 +0000
@@ -9,15 +9,15 @@
9from lazr.enum import DBEnumeratedType, DBItem, EnumeratedType, Item9from lazr.enum import DBEnumeratedType, DBItem, EnumeratedType, Item
1010
11from canonical.launchpad import _11from canonical.launchpad import _
12from canonical.launchpad.fields import ParticipatingPersonChoice
12from lp.registry.interfaces.sourcepackage import ISourcePackage13from lp.registry.interfaces.sourcepackage import ISourcePackage
13from lp.translations.interfaces.translationfileformat import (14from lp.translations.interfaces.translationfileformat import (
14 TranslationFileFormat)15 TranslationFileFormat)
15from lp.registry.interfaces.distroseries import IDistroSeries16from lp.registry.interfaces.distroseries import IDistroSeries
16from lp.registry.interfaces.person import IPerson
17from lp.registry.interfaces.productseries import IProductSeries17from lp.registry.interfaces.productseries import IProductSeries
1818
19from lazr.restful.interface import copy_field19from lazr.restful.interface import copy_field
20from lazr.restful.fields import Reference, ReferenceChoice20from lazr.restful.fields import Reference
21from lazr.restful.declarations import (21from lazr.restful.declarations import (
22 collection_default_content, exported, export_as_webservice_collection,22 collection_default_content, exported, export_as_webservice_collection,
23 export_as_webservice_entry, export_read_operation, operation_parameters,23 export_as_webservice_entry, export_read_operation, operation_parameters,
@@ -152,9 +152,8 @@
152 required=True))152 required=True))
153153
154 importer = exported(154 importer = exported(
155 ReferenceChoice(155 ParticipatingPersonChoice(
156 title=_("Uploader"),156 title=_("Uploader"),
157 schema=IPerson,
158 required=True,157 required=True,
159 readonly=True,158 readonly=True,
160 description=_(159 description=_(
161160
=== modified file 'lib/lp/translations/model/translationimportqueue.py'
--- lib/lp/translations/model/translationimportqueue.py 2009-09-28 09:46:27 +0000
+++ lib/lp/translations/model/translationimportqueue.py 2009-10-05 11:36:16 +0000
@@ -38,7 +38,8 @@
38from lp.registry.interfaces.distribution import IDistribution38from lp.registry.interfaces.distribution import IDistribution
39from lp.registry.interfaces.distroseries import (39from lp.registry.interfaces.distroseries import (
40 IDistroSeries, DistroSeriesStatus)40 IDistroSeries, DistroSeriesStatus)
41from lp.registry.interfaces.person import IPerson41from lp.registry.interfaces.person import (
42 IPerson, validate_person_not_private_membership)
42from lp.registry.interfaces.product import IProduct43from lp.registry.interfaces.product import IProduct
43from lp.registry.interfaces.productseries import IProductSeries44from lp.registry.interfaces.productseries import IProductSeries
44from lp.registry.interfaces.sourcepackage import ISourcePackage45from lp.registry.interfaces.sourcepackage import ISourcePackage
@@ -61,7 +62,6 @@
61from lp.translations.utilities.gettext_po_importer import (62from lp.translations.utilities.gettext_po_importer import (
62 GettextPOImporter)63 GettextPOImporter)
63from canonical.librarian.interfaces import ILibrarianClient64from canonical.librarian.interfaces import ILibrarianClient
64from lp.registry.interfaces.person import validate_public_person
6565
6666
67# Period to wait before entries with terminal statuses are removed from67# Period to wait before entries with terminal statuses are removed from
@@ -120,7 +120,8 @@
120 notNull=False)120 notNull=False)
121 importer = ForeignKey(121 importer = ForeignKey(
122 dbName='importer', foreignKey='Person',122 dbName='importer', foreignKey='Person',
123 storm_validator=validate_public_person, notNull=True)123 storm_validator=validate_person_not_private_membership,
124 notNull=True)
124 dateimported = UtcDateTimeCol(dbName='dateimported', notNull=True,125 dateimported = UtcDateTimeCol(dbName='dateimported', notNull=True,
125 default=DEFAULT)126 default=DEFAULT)
126 sourcepackagename_id = Int(name='sourcepackagename', allow_none=True)127 sourcepackagename_id = Int(name='sourcepackagename', allow_none=True)