Merge lp:~abentley/launchpad/limit-product-info-types into lp:launchpad

Proposed by Aaron Bentley on 2012-10-03
Status: Merged
Merged at revision: 16085
Proposed branch: lp:~abentley/launchpad/limit-product-info-types
Merge into: lp:launchpad
Diff against target: 197 lines (+103/-8)
3 files modified
lib/lp/registry/model/product.py (+30/-2)
lib/lp/registry/tests/test_product.py (+71/-5)
lib/lp/scripts/tests/test_garbo.py (+2/-1)
To merge this branch: bzr merge lp:~abentley/launchpad/limit-product-info-types
Reviewer Review Type Date Requested Status
Deryck Hodge (community) 2012-10-03 Approve on 2012-10-03
Review via email: mp+127814@code.launchpad.net

Commit Message

Ensure only valid Product.information_type values are used.

Description of the Change

= Summary =
Ensure only valid information_types are used for Product.

== Proposed fix ==
Use a Storm validator for Product.information_type.

== Pre-implementation notes ==
Discussed with deryck

== LOC Rationale ==
Part of Private Projects

== Implementation details ==
The storm validator ensures that values outside PUBLIC_PROPRIETARY are not accepted. For mutations, it ensures that PROPRIETARY values can only be set if there is a commercial subscription. For creation, createProject ensures that PROPRIETARY values are only used with Other/Proprietary licenses, which imply a commercial subscription.

== Tests ==
bin/test -t TestProduct registry.tests.test_product

== Demo and Q/A ==
Modifying an existing Product should not complain about a NULL information_type.

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/registry/configure.zcml
  lib/lp/registry/interfaces/product.py
  lib/lp/app/interfaces/informationtype.py
  lib/lp/app/model/launchpad.py
  lib/lp/testing/factory.py
  lib/lp/scripts/garbo.py
  lib/lp/blueprints/model/specification.py
  lib/lp/scripts/tests/test_garbo.py
  lib/lp/bugs/model/bug.py
  lib/lp/registry/model/product.py
  lib/lp/registry/tests/test_product.py

To post a comment you must log in.
Deryck Hodge (deryck) wrote :

Looks nice. I like the use of the storm validator here.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/registry/model/product.py'
2--- lib/lp/registry/model/product.py 2012-10-03 13:05:31 +0000
3+++ lib/lp/registry/model/product.py 2012-10-03 17:43:22 +0000
4@@ -69,6 +69,8 @@
5 FREE_INFORMATION_TYPES,
6 InformationType,
7 PRIVATE_INFORMATION_TYPES,
8+ PUBLIC_PROPRIETARY_INFORMATION_TYPES,
9+ PROPRIETARY_INFORMATION_TYPES,
10 service_uses_launchpad,
11 ServiceUsage,
12 )
13@@ -127,7 +129,10 @@
14 BugSharingPolicy,
15 SpecificationSharingPolicy,
16 )
17-from lp.registry.errors import CommercialSubscribersOnly
18+from lp.registry.errors import (
19+ CannotChangeInformationType,
20+ CommercialSubscribersOnly,
21+ )
22 from lp.registry.interfaces.accesspolicy import (
23 IAccessPolicyArtifactSource,
24 IAccessPolicyGrantSource,
25@@ -415,8 +420,22 @@
26 """
27 pass
28
29+ def _valid_product_information_type(self, attr, value):
30+ if value not in PUBLIC_PROPRIETARY_INFORMATION_TYPES:
31+ raise CannotChangeInformationType('Not supported for Projects.')
32+ # Proprietary check works only after creation, because during
33+ # creation, has_commercial_subscription cannot give the right value
34+ # and triggers an inappropriate DB flush.
35+ if (not self._SO_creating and value in PROPRIETARY_INFORMATION_TYPES
36+ and not self.has_current_commercial_subscription):
37+ raise CommercialSubscribersOnly(
38+ 'A valid commercial subscription is required for private'
39+ ' Projects.')
40+ return value
41+
42 information_type = EnumCol(
43- enum=InformationType, default=InformationType.PUBLIC)
44+ enum=InformationType, default=InformationType.PUBLIC,
45+ storm_validator=_valid_product_information_type)
46
47 security_contact = None
48
49@@ -1603,6 +1622,15 @@
50 licenses = set()
51 if information_type is None:
52 information_type = InformationType.PUBLIC
53+ if information_type in PROPRIETARY_INFORMATION_TYPES:
54+ # This check is skipped in _valid_product_information_type during
55+ # creation, so done here. It predicts whether a commercial
56+ # subscription will be generated based on the selected license,
57+ # duplicating product._setLicenses
58+ if License.OTHER_PROPRIETARY not in licenses:
59+ raise CommercialSubscribersOnly(
60+ 'A valid commercial subscription is required for private'
61+ ' Projects.')
62 product = Product(
63 owner=owner, registrant=registrant, name=name,
64 displayname=displayname, title=title, project=project,
65
66=== modified file 'lib/lp/registry/tests/test_product.py'
67--- lib/lp/registry/tests/test_product.py 2012-10-02 18:44:49 +0000
68+++ lib/lp/registry/tests/test_product.py 2012-10-03 17:43:22 +0000
69@@ -9,6 +9,7 @@
70 import pytz
71 from storm.locals import Store
72 from testtools.matchers import MatchesAll
73+from testtools.testcase import ExpectedException
74 import transaction
75 from zope.component import getUtility
76 from zope.lifecycleevent.interfaces import IObjectModifiedEvent
77@@ -19,6 +20,8 @@
78 from lp.app.enums import (
79 FREE_INFORMATION_TYPES,
80 InformationType,
81+ PUBLIC_PROPRIETARY_INFORMATION_TYPES,
82+ PROPRIETARY_INFORMATION_TYPES,
83 ServiceUsage,
84 )
85 from lp.app.interfaces.launchpad import (
86@@ -41,6 +44,7 @@
87 SpecificationSharingPolicy,
88 )
89 from lp.registry.errors import (
90+ CannotChangeInformationType,
91 CommercialSubscribersOnly,
92 InclusiveTeamLinkageError,
93 )
94@@ -393,10 +397,25 @@
95 expected = [InformationType.PROPRIETARY]
96 self.assertContentEqual(expected, [policy.type for policy in aps])
97
98+ def createProduct(self, information_type=None, license=None):
99+ # convenience method for testing IProductSet.createProduct rather than
100+ # self.factory.makeProduct
101+ owner = self.factory.makePerson()
102+ kwargs = {}
103+ if information_type is not None:
104+ kwargs['information_type'] = information_type
105+ if license is not None:
106+ kwargs['licenses'] = [license]
107+ with person_logged_in(owner):
108+ return getUtility(IProductSet).createProduct(
109+ owner, self.factory.getUniqueString('product'),
110+ 'Fnord', 'Fnord', 'test 1', 'test 2', **kwargs)
111+
112 def test_product_information_type(self):
113 # Product is created with specified information_type
114- product = self.factory.makeProduct(
115- information_type=InformationType.EMBARGOED)
116+ product = self.createProduct(
117+ information_type=InformationType.EMBARGOED,
118+ license=License.OTHER_PROPRIETARY)
119 self.assertEqual(InformationType.EMBARGOED, product.information_type)
120 # Owner can set information_type
121 with person_logged_in(product.owner):
122@@ -411,11 +430,58 @@
123
124 def test_product_information_type_default(self):
125 # Default information_type is PUBLIC
126- owner = self.factory.makePerson()
127- product = getUtility(IProductSet).createProduct(
128- owner, 'fnord', 'Fnord', 'Fnord', 'test 1', 'test 2')
129+ product = self.createProduct()
130 self.assertEqual(InformationType.PUBLIC, product.information_type)
131
132+ invalid_information_types = [info_type for info_type in
133+ InformationType.items if info_type not in
134+ PUBLIC_PROPRIETARY_INFORMATION_TYPES]
135+
136+ def test_product_information_type_init_invalid_values(self):
137+ # Cannot create Product.information_type with invalid values.
138+ for info_type in self.invalid_information_types:
139+ with ExpectedException(
140+ CannotChangeInformationType, 'Not supported for Projects.'):
141+ self.createProduct(information_type=info_type)
142+
143+ def test_product_information_type_set_invalid_values(self):
144+ # Cannot set Product.information_type to invalid values.
145+ product = self.factory.makeProduct()
146+ for info_type in self.invalid_information_types:
147+ with ExpectedException(
148+ CannotChangeInformationType, 'Not supported for Projects.'):
149+ with person_logged_in(product.owner):
150+ product.information_type = info_type
151+
152+ def test_product_information_set_proprietary_requires_commercial(self):
153+ # Cannot set Product.information_type to proprietary values if no
154+ # commercial subscription.
155+ product = self.factory.makeProduct()
156+ self.useContext(person_logged_in(product.owner))
157+ for info_type in PROPRIETARY_INFORMATION_TYPES:
158+ with ExpectedException(
159+ CommercialSubscribersOnly,
160+ 'A valid commercial subscription is required for private'
161+ ' Projects.'):
162+ product.information_type = info_type
163+ product.redeemSubscriptionVoucher(
164+ 'hello', product.owner, product.owner, 1)
165+ for info_type in PROPRIETARY_INFORMATION_TYPES:
166+ product.information_type = info_type
167+
168+ def test_product_information_init_proprietary_requires_commercial(self):
169+ # Cannot create a product with proprietary types without specifying
170+ # Other/Proprietary license.
171+ for info_type in PROPRIETARY_INFORMATION_TYPES:
172+ with ExpectedException(
173+ CommercialSubscribersOnly,
174+ 'A valid commercial subscription is required for private'
175+ ' Projects.'):
176+ self.createProduct(info_type)
177+ for info_type in PROPRIETARY_INFORMATION_TYPES:
178+ product = self.createProduct(info_type, License.OTHER_PROPRIETARY)
179+ self.assertEqual(info_type, product.information_type)
180+
181
182 class TestProductBugInformationTypes(TestCaseWithFactory):
183
184
185=== modified file 'lib/lp/scripts/tests/test_garbo.py'
186--- lib/lp/scripts/tests/test_garbo.py 2012-10-02 19:25:44 +0000
187+++ lib/lp/scripts/tests/test_garbo.py 2012-10-03 17:43:22 +0000
188@@ -1068,7 +1068,8 @@
189 store.flush()
190 # Make a new product without an information_type.
191 product = self.factory.makeProduct()
192- removeSecurityProxy(product).information_type = None
193+ store.execute(Update(
194+ {Product.information_type: None}, Product.id == product.id))
195 store.flush()
196 self.assertEqual(1, store.find(Product,
197 Product.information_type == None).count())