Colin Watson has proposed merging ~cjwatson/launchpad:commercial-subscription-distribution into launchpad:master.
Commit message: Add basic support for commercial subscriptions to distributions Requested reviews: Launchpad code reviewers (launchpad-reviewers) For more details, see: https://code.launchpad.net/~cjwatson/launchpad/+git/launchpad/+merge/415306 These are used to control the ability to use privacy features. There isn't much in the way of tests here yet, because writing those needs privacy support in the `Distribution` model which is a rather more involved affair, but this gets some of the simple bits out of the way. -- Your team Launchpad code reviewers is requested to review the proposed merge of ~cjwatson/launchpad:commercial-subscription-distribution into launchpad:master.
diff --git a/lib/lp/_schema_circular_imports.py b/lib/lp/_schema_circular_imports.py index 7c99b77..9f981c0 100644 --- a/lib/lp/_schema_circular_imports.py +++ b/lib/lp/_schema_circular_imports.py @@ -429,6 +429,11 @@ patch_reference_property(IBuildFarmJob, 'buildqueue_record', IBuildQueue) # IComment patch_reference_property(IComment, 'comment_author', IPerson) +# ICommercialSubscription +patch_reference_property(ICommercialSubscription, 'product', IProduct) +patch_reference_property( + ICommercialSubscription, 'distribution', IDistribution) + # IDistribution patch_collection_property(IDistribution, 'series', IDistroSeries) patch_collection_property(IDistribution, 'derivatives', IDistroSeries) diff --git a/lib/lp/bugs/browser/tests/test_bugtarget_filebug.py b/lib/lp/bugs/browser/tests/test_bugtarget_filebug.py index 3a693b3..3d4928f 100644 --- a/lib/lp/bugs/browser/tests/test_bugtarget_filebug.py +++ b/lib/lp/bugs/browser/tests/test_bugtarget_filebug.py @@ -385,7 +385,7 @@ class TestFileBugViewBase(FileBugViewMixin, TestCaseWithFactory): if bug_sharing_policy: if not IProduct.providedBy(target): raise ValueError("Only Product supports this.") - self.factory.makeCommercialSubscription(product=target) + self.factory.makeCommercialSubscription(pillar=target) with person_logged_in(owner): target.setBugSharingPolicy(bug_sharing_policy) with person_logged_in(owner): @@ -476,7 +476,7 @@ class TestFileBugViewBase(FileBugViewMixin, TestCaseWithFactory): # The vocabulary for information_type when filing a bug is created # correctly for a project with a proprietary sharing policy. product = self.factory.makeProduct(official_malone=True) - self.factory.makeCommercialSubscription(product=product) + self.factory.makeCommercialSubscription(pillar=product) with person_logged_in(product.owner): product.setBugSharingPolicy(BugSharingPolicy.PROPRIETARY) view = create_initialized_view( @@ -777,7 +777,7 @@ class TestFileBugForNonBugSupervisors(TestCaseWithFactory): } product = self.factory.makeProduct(official_malone=True) if bug_sharing_policy: - self.factory.makeCommercialSubscription(product=product) + self.factory.makeCommercialSubscription(pillar=product) with person_logged_in(product.owner): product.setBugSharingPolicy(bug_sharing_policy) anyone = self.factory.makePerson() diff --git a/lib/lp/bugs/mail/tests/test_handler.py b/lib/lp/bugs/mail/tests/test_handler.py index 20dadf6..65033d2 100644 --- a/lib/lp/bugs/mail/tests/test_handler.py +++ b/lib/lp/bugs/mail/tests/test_handler.py @@ -249,7 +249,7 @@ class MaloneHandlerProcessTestCase(TestCaseWithFactory): def test_new_bug_with_sharing_policy_proprietary(self): project = self.factory.makeProduct(name='fnord') - self.factory.makeCommercialSubscription(product=project) + self.factory.makeCommercialSubscription(pillar=project) with person_logged_in(project.owner): project.setBugSharingPolicy(BugSharingPolicy.PROPRIETARY) transaction.commit() diff --git a/lib/lp/code/browser/tests/test_branch.py b/lib/lp/code/browser/tests/test_branch.py index ceaac7c..ac77d79 100644 --- a/lib/lp/code/browser/tests/test_branch.py +++ b/lib/lp/code/browser/tests/test_branch.py @@ -1225,7 +1225,7 @@ class TestBranchEditViewInformationTypes(TestCaseWithFactory): # proprietary allow only embargoed and proprietary types. owner = self.factory.makePerson() product = self.factory.makeProduct(owner=owner) - self.factory.makeCommercialSubscription(product=product) + self.factory.makeCommercialSubscription(pillar=product) with person_logged_in(owner): product.setBranchSharingPolicy( BranchSharingPolicy.EMBARGOED_OR_PROPRIETARY) @@ -1240,7 +1240,7 @@ class TestBranchEditViewInformationTypes(TestCaseWithFactory): # allow only the proprietary type. owner = self.factory.makePerson() product = self.factory.makeProduct(owner=owner) - self.factory.makeCommercialSubscription(product=product) + self.factory.makeCommercialSubscription(pillar=product) with person_logged_in(owner): product.setBranchSharingPolicy(BranchSharingPolicy.PROPRIETARY) branch = self.factory.makeBranch( diff --git a/lib/lp/code/browser/tests/test_gitrepository.py b/lib/lp/code/browser/tests/test_gitrepository.py index 6b3e480..05c7043 100644 --- a/lib/lp/code/browser/tests/test_gitrepository.py +++ b/lib/lp/code/browser/tests/test_gitrepository.py @@ -1309,7 +1309,7 @@ class TestGitRepositoryEditViewInformationTypes(TestCaseWithFactory): # types. owner = self.factory.makePerson() project = self.factory.makeProduct(owner=owner) - self.factory.makeCommercialSubscription(product=project) + self.factory.makeCommercialSubscription(pillar=project) with person_logged_in(owner): project.setBranchSharingPolicy( BranchSharingPolicy.EMBARGOED_OR_PROPRIETARY) @@ -1325,7 +1325,7 @@ class TestGitRepositoryEditViewInformationTypes(TestCaseWithFactory): # proprietary allow only the proprietary type. owner = self.factory.makePerson() product = self.factory.makeProduct(owner=owner) - self.factory.makeCommercialSubscription(product=product) + self.factory.makeCommercialSubscription(pillar=product) with person_logged_in(owner): product.setBranchSharingPolicy(BranchSharingPolicy.PROPRIETARY) repository = self.factory.makeGitRepository( diff --git a/lib/lp/code/model/tests/test_branchnamespace.py b/lib/lp/code/model/tests/test_branchnamespace.py index d0dad62..aff13b1 100644 --- a/lib/lp/code/model/tests/test_branchnamespace.py +++ b/lib/lp/code/model/tests/test_branchnamespace.py @@ -391,7 +391,7 @@ class TestProjectBranchNamespacePrivacyWithInformationType( if person is None: person = self.factory.makePerson() product = self.factory.makeProduct() - self.factory.makeCommercialSubscription(product=product) + self.factory.makeCommercialSubscription(pillar=product) with person_logged_in(product.owner): product.setBranchSharingPolicy(sharing_policy) namespace = ProjectBranchNamespace(person, product) diff --git a/lib/lp/code/model/tests/test_gitnamespace.py b/lib/lp/code/model/tests/test_gitnamespace.py index eb11aa9..ac469e9 100644 --- a/lib/lp/code/model/tests/test_gitnamespace.py +++ b/lib/lp/code/model/tests/test_gitnamespace.py @@ -620,7 +620,7 @@ class TestProjectGitNamespacePrivacyWithInformationType(TestCaseWithFactory): if person is None: person = self.factory.makePerson() project = self.factory.makeProduct() - self.factory.makeCommercialSubscription(product=project) + self.factory.makeCommercialSubscription(pillar=project) with person_logged_in(project.owner): project.setBranchSharingPolicy(sharing_policy) namespace = ProjectGitNamespace(person, project) diff --git a/lib/lp/registry/browser/configure.zcml b/lib/lp/registry/browser/configure.zcml index 63443a0..07fada1 100644 --- a/lib/lp/registry/browser/configure.zcml +++ b/lib/lp/registry/browser/configure.zcml @@ -654,7 +654,7 @@ <browser:url for="lp.registry.interfaces.commercialsubscription.ICommercialSubscription" path_expression="string:+commercialsubscription/${id}" - attribute_to_parent="product" + attribute_to_parent="pillar" /> <browser:url for="lp.registry.interfaces.jabber.IJabberID" diff --git a/lib/lp/registry/browser/distribution.py b/lib/lp/registry/browser/distribution.py index 4f11395..6ae28d7 100644 --- a/lib/lp/registry/browser/distribution.py +++ b/lib/lp/registry/browser/distribution.py @@ -196,6 +196,10 @@ class DistributionNavigation( def traverse_archive(self, name): return self.context.getArchive(name) + @stepthrough('+commercialsubscription') + def traverse_commercialsubscription(self, name): + return self.context.commercial_subscription + def _resolveSeries(self, name): try: return self.context[name], False diff --git a/lib/lp/registry/interfaces/commercialsubscription.py b/lib/lp/registry/interfaces/commercialsubscription.py index 81b064a..62c1ec1 100644 --- a/lib/lp/registry/interfaces/commercialsubscription.py +++ b/lib/lp/registry/interfaces/commercialsubscription.py @@ -12,7 +12,10 @@ from lazr.restful.declarations import ( exported_as_webservice_entry, ) from lazr.restful.fields import ReferenceChoice -from zope.interface import Interface +from zope.interface import ( + Attribute, + Interface, + ) from zope.schema import ( Bool, Datetime, @@ -38,15 +41,30 @@ class ICommercialSubscription(Interface): product = exported( ReferenceChoice( title=_("Product which has commercial subscription"), - required=True, + required=False, readonly=True, vocabulary='Product', - # Really IProduct. See lp/registry/interfaces/product.py + # Really IProduct, patched in _schema_circular_imports.py. schema=Interface, description=_( "Project for which this commercial subscription is " "applied."))) + distribution = exported( + ReferenceChoice( + title=_("Distribution which has commercial subscription"), + required=False, + readonly=True, + vocabulary='Distribution', + # Really IDistribution, patched in _schema_circular_imports.py. + schema=Interface, + description=_( + "Distribution for which this commercial subscription is " + "applied."))) + + pillar = Attribute( + "Pillar for which this commercial subscription is applied.") + date_created = exported( Datetime( title=_('Date Created'), diff --git a/lib/lp/registry/interfaces/distribution.py b/lib/lp/registry/interfaces/distribution.py index 69d9540..6c9b00d 100644 --- a/lib/lp/registry/interfaces/distribution.py +++ b/lib/lp/registry/interfaces/distribution.py @@ -82,6 +82,9 @@ from lp.registry.enums import ( VCSType, ) from lp.registry.interfaces.announcement import IMakesAnnouncements +from lp.registry.interfaces.commercialsubscription import ( + ICommercialSubscription, + ) from lp.registry.interfaces.distributionmirror import IDistributionMirror from lp.registry.interfaces.distroseries import DistroSeriesNameField from lp.registry.interfaces.karma import IKarmaContext @@ -449,6 +452,13 @@ class IDistributionPublic( "to a different canonical URL."), readonly=False, required=False)) + commercial_subscription = exported(Reference( + ICommercialSubscription, + title=_("Commercial subscriptions"), + description=_( + "An object which contains the timeframe and the voucher code of a " + "subscription."))) + def getArchiveIDList(archive=None): """Return a list of archive IDs suitable for sqlvalues() or quote(). diff --git a/lib/lp/registry/interfaces/product.py b/lib/lp/registry/interfaces/product.py index 120a9fe..2d0c179 100644 --- a/lib/lp/registry/interfaces/product.py +++ b/lib/lp/registry/interfaces/product.py @@ -1190,5 +1190,3 @@ from lp.registry.interfaces.distributionsourcepackage import ( # noqa: E402 patch_reference_property( IDistributionSourcePackage, 'upstream_product', IProduct) - -patch_reference_property(ICommercialSubscription, 'product', IProduct) diff --git a/lib/lp/registry/model/commercialsubscription.py b/lib/lp/registry/model/commercialsubscription.py index 1620c27..fc467b9 100644 --- a/lib/lp/registry/model/commercialsubscription.py +++ b/lib/lp/registry/model/commercialsubscription.py @@ -21,7 +21,9 @@ from lp.registry.errors import CannotDeleteCommercialSubscription from lp.registry.interfaces.commercialsubscription import ( ICommercialSubscription, ) +from lp.registry.interfaces.distribution import IDistribution from lp.registry.interfaces.person import validate_public_person +from lp.registry.interfaces.product import IProduct from lp.services.database.constants import UTC_NOW from lp.services.database.stormbase import StormBase @@ -33,9 +35,12 @@ class CommercialSubscription(StormBase): id = Int(primary=True) - product_id = Int(name='product', allow_none=False) + product_id = Int(name='product', allow_none=True) product = Reference(product_id, 'Product.id') + distribution_id = Int(name='distribution', allow_none=True) + distribution = Reference(distribution_id, 'Distribution.id') + date_created = DateTime(tzinfo=pytz.UTC, allow_none=False, default=UTC_NOW) date_last_modified = DateTime( tzinfo=pytz.UTC, allow_none=False, default=UTC_NOW) @@ -53,10 +58,17 @@ class CommercialSubscription(StormBase): sales_system_id = Unicode(allow_none=False) whiteboard = Unicode(default=None) - def __init__(self, product, date_starts, date_expires, registrant, + def __init__(self, pillar, date_starts, date_expires, registrant, purchaser, sales_system_id, whiteboard): super().__init__() - self.product = product + if IProduct.providedBy(pillar): + self.product = pillar + self.distribution = None + elif IDistribution.providedBy(pillar): + self.product = None + self.distribution = pillar + else: + raise AssertionError("Unknown pillar: %r" % pillar) self.date_starts = date_starts self.date_expires = date_expires self.registrant = registrant @@ -65,6 +77,11 @@ class CommercialSubscription(StormBase): self.whiteboard = whiteboard @property + def pillar(self): + return ( + self.product if self.product_id is not None else self.distribution) + + @property def is_active(self): """See `ICommercialSubscription`""" now = datetime.datetime.now(pytz.timezone('UTC')) diff --git a/lib/lp/registry/model/distribution.py b/lib/lp/registry/model/distribution.py index b993152..867c4df 100644 --- a/lib/lp/registry/model/distribution.py +++ b/lib/lp/registry/model/distribution.py @@ -120,6 +120,7 @@ from lp.registry.interfaces.role import IPersonRoles from lp.registry.interfaces.series import SeriesStatus from lp.registry.interfaces.sourcepackagename import ISourcePackageName from lp.registry.model.announcement import MakesAnnouncements +from lp.registry.model.commercialsubscription import CommercialSubscription from lp.registry.model.distributionmirror import ( DistributionMirror, MirrorCDImageDistroSeries, @@ -343,6 +344,11 @@ class Distribution(SQLBase, BugTargetBase, MakesAnnouncements, # Sharing policy for distributions is always PUBLIC. return SpecificationSharingPolicy.PUBLIC + @cachedproperty + def commercial_subscription(self): + return IStore(CommercialSubscription).find( + CommercialSubscription, distribution=self).one() + @property def uploaders(self): """See `IDistribution`.""" diff --git a/lib/lp/registry/model/product.py b/lib/lp/registry/model/product.py index f213c1d..0d9235c 100644 --- a/lib/lp/registry/model/product.py +++ b/lib/lp/registry/model/product.py @@ -985,7 +985,7 @@ class Product(SQLBase, BugTargetBase, MakesAnnouncements, "Complimentary 30 day subscription. -- Launchpad %s" % now.date().isoformat()) subscription = CommercialSubscription( - product=self, date_starts=now, date_expires=date_expires, + pillar=self, date_starts=now, date_expires=date_expires, registrant=lp_janitor, purchaser=lp_janitor, sales_system_id=sales_system_id, whiteboard=whiteboard) get_property_cache(self).commercial_subscription = subscription diff --git a/lib/lp/registry/stories/webservice/xx-distribution.txt b/lib/lp/registry/stories/webservice/xx-distribution.txt index f0748c2..4f98323 100644 --- a/lib/lp/registry/stories/webservice/xx-distribution.txt +++ b/lib/lp/registry/stories/webservice/xx-distribution.txt @@ -28,6 +28,7 @@ And for every distribution we publish most of its attributes. bug_reporting_guidelines: None bug_supervisor_link: None cdimage_mirrors_collection_link: 'http://.../ubuntu/cdimage_mirrors' + commercial_subscription_link: None current_series_link: 'http://.../ubuntu/hoary' date_created: '2006-10-16T18:31:43.415195+00:00' default_traversal_policy: 'Series' diff --git a/lib/lp/registry/tests/test_product.py b/lib/lp/registry/tests/test_product.py index d0eb5a6..c037549 100644 --- a/lib/lp/registry/tests/test_product.py +++ b/lib/lp/registry/tests/test_product.py @@ -1237,7 +1237,7 @@ class TestProductBugInformationTypes(TestCaseWithFactory): def makeProductWithPolicy(self, bug_sharing_policy): product = self.factory.makeProduct() - self.factory.makeCommercialSubscription(product=product) + self.factory.makeCommercialSubscription(pillar=product) with person_logged_in(product.owner): product.setBugSharingPolicy(bug_sharing_policy) return product @@ -1290,7 +1290,7 @@ class TestProductSpecificationPolicyAndInformationTypes(TestCaseWithFactory): def makeProductWithPolicy(self, specification_sharing_policy): product = self.factory.makeProduct() - self.factory.makeCommercialSubscription(product=product) + self.factory.makeCommercialSubscription(pillar=product) with person_logged_in(product.owner): product.setSpecificationSharingPolicy( specification_sharing_policy) @@ -1658,7 +1658,7 @@ class BaseSharingPolicyTests: def test_commercial_admin_can_set_policy(self): # Commercial admins can set sharing policies for commercial projects. - self.factory.makeCommercialSubscription(product=self.product) + self.factory.makeCommercialSubscription(pillar=self.product) self.setSharingPolicy(self.public_policy, self.commercial_admin) self.assertEqual(self.public_policy, self.getSharingPolicy()) @@ -1686,7 +1686,7 @@ class BaseSharingPolicyTests: def test_proprietary_allowed_with_commercial_sub(self): # All policies are valid when there's a current commercial # subscription. - self.factory.makeCommercialSubscription(product=self.product) + self.factory.makeCommercialSubscription(pillar=self.product) for policy in self.enum.items: self.setSharingPolicy(policy, self.commercial_admin) self.assertEqual(policy, self.getSharingPolicy()) @@ -1695,7 +1695,7 @@ class BaseSharingPolicyTests: # Setting a policy that allows Proprietary creates a # corresponding access policy and shares it with the the # maintainer. - self.factory.makeCommercialSubscription(product=self.product) + self.factory.makeCommercialSubscription(pillar=self.product) self.assertEqual( [InformationType.PRIVATESECURITY, InformationType.USERDATA], [policy.type for policy in @@ -1730,7 +1730,7 @@ class BaseSharingPolicyTests: for ap in ap_source.findByPillar([pillar])] # Now change the sharing policies to PROPRIETARY - self.factory.makeCommercialSubscription(product=product) + self.factory.makeCommercialSubscription(pillar=product) with person_logged_in(product.owner): product.setBugSharingPolicy(BugSharingPolicy.PROPRIETARY) # Just bug sharing policy has been changed so all previous policy @@ -1817,7 +1817,7 @@ class ProductBranchSharingPolicyTestCase(BaseSharingPolicyTests, # Setting a policy that allows Embargoed creates a # corresponding access policy and shares it with the the # maintainer. - self.factory.makeCommercialSubscription(product=self.product) + self.factory.makeCommercialSubscription(pillar=self.product) self.assertEqual( [InformationType.PRIVATESECURITY, InformationType.USERDATA], [policy.type for policy in diff --git a/lib/lp/registry/tests/test_product_webservice.py b/lib/lp/registry/tests/test_product_webservice.py index 4f6e465..5808836 100644 --- a/lib/lp/registry/tests/test_product_webservice.py +++ b/lib/lp/registry/tests/test_product_webservice.py @@ -57,7 +57,7 @@ class TestProduct(TestCaseWithFactory): # branch_sharing_policy can be set via the API. product = self.factory.makeProduct() owner = product.owner - self.factory.makeCommercialSubscription(product=product) + self.factory.makeCommercialSubscription(pillar=product) webservice = webservice_for_person( owner, permission=OAuthPermission.WRITE_PRIVATE) response = self.patch( @@ -88,7 +88,7 @@ class TestProduct(TestCaseWithFactory): # bug_sharing_policy can be set via the API. product = self.factory.makeProduct() owner = product.owner - self.factory.makeCommercialSubscription(product=product) + self.factory.makeCommercialSubscription(pillar=product) webservice = webservice_for_person( product.owner, permission=OAuthPermission.WRITE_PRIVATE) response = self.patch( diff --git a/lib/lp/scripts/tests/test_garbo.py b/lib/lp/scripts/tests/test_garbo.py index 99f36cd..70384af 100644 --- a/lib/lp/scripts/tests/test_garbo.py +++ b/lib/lp/scripts/tests/test_garbo.py @@ -1361,7 +1361,7 @@ class TestGarbo(FakeAdapterMixin, TestCaseWithFactory): # in use by artifacts or allowed by the project sharing policy. switch_dbuser('testadmin') product = self.factory.makeProduct() - self.factory.makeCommercialSubscription(product=product) + self.factory.makeCommercialSubscription(pillar=product) self.factory.makeAccessPolicy(product, InformationType.PROPRIETARY) naked_product = removeSecurityProxy(product) naked_product.bug_sharing_policy = BugSharingPolicy.PROPRIETARY diff --git a/lib/lp/testing/factory.py b/lib/lp/testing/factory.py index 0ef41bb..b098cba 100644 --- a/lib/lp/testing/factory.py +++ b/lib/lp/testing/factory.py @@ -4691,26 +4691,32 @@ class BareLaunchpadObjectFactory(ObjectFactory): } return fileupload - def makeCommercialSubscription(self, product, expired=False, + def makeCommercialSubscription(self, pillar, expired=False, voucher_id='new'): - """Create a commercial subscription for the given product.""" + """Create a commercial subscription for the given pillar.""" + if IProduct.providedBy(pillar): + find_kwargs = {"product": pillar} + elif IDistribution.providedBy(pillar): + find_kwargs = {"distribution": pillar} + else: + raise AssertionError("Unknown pillar: %r" % pillar) if IStore(CommercialSubscription).find( - CommercialSubscription, product=product).one() is not None: + CommercialSubscription, **find_kwargs).one() is not None: raise AssertionError( - "The product under test already has a CommercialSubscription.") + "The pillar under test already has a CommercialSubscription.") if expired: expiry = datetime.now(pytz.UTC) - timedelta(days=1) else: expiry = datetime.now(pytz.UTC) + timedelta(days=30) commercial_subscription = CommercialSubscription( - product=product, + pillar=pillar, date_starts=datetime.now(pytz.UTC) - timedelta(days=90), date_expires=expiry, - registrant=product.owner, - purchaser=product.owner, + registrant=pillar.owner, + purchaser=pillar.owner, sales_system_id=voucher_id, whiteboard='') - del get_property_cache(product).commercial_subscription + del get_property_cache(pillar).commercial_subscription return commercial_subscription def grantCommercialSubscription(self, person):
_______________________________________________ Mailing list: https://launchpad.net/~launchpad-reviewers Post to : launchpad-reviewers@lists.launchpad.net Unsubscribe : https://launchpad.net/~launchpad-reviewers More help : https://help.launchpad.net/ListHelp