Merge lp:~rharding/launchpad/store_edit_product_info_type into lp:launchpad

Proposed by Richard Harding
Status: Merged
Approved by: Richard Harding
Approved revision: no longer in the source branch.
Merged at revision: 16120
Proposed branch: lp:~rharding/launchpad/store_edit_product_info_type
Merge into: lp:launchpad
Prerequisite: lp:~rharding/launchpad/edit_product_info_type
Diff against target: 324 lines (+162/-33)
6 files modified
lib/lp/registry/browser/tests/test_product.py (+68/-0)
lib/lp/registry/javascript/product_views.js (+4/-2)
lib/lp/registry/model/product.py (+41/-9)
lib/lp/registry/stories/webservice/xx-project-registry.txt (+1/-1)
lib/lp/registry/tests/test_product.py (+46/-19)
lib/lp/scripts/garbo.py (+2/-2)
To merge this branch: bzr merge lp:~rharding/launchpad/store_edit_product_info_type
Reviewer Review Type Date Requested Status
Aaron Bentley (community) Approve
Review via email: mp+128288@code.launchpad.net

Commit message

Implement updating based on changing the information type on Product edit.

Description of the change

= Summary =

This is the second branch in allowing edits to the Product. It updates the
setter for the information type to update policies as required.

== Pre Implementation ==

Talked with Aaron about updating the policies correctly.

== Implementation Notes ==

The first update is to change the validation to make sure we allow the
_setLicense to create a commercial subscription if we've not had one
previously. This allows for a Public project to go to a Private information
type as an edit.

The second update makes sure that after we change the information type that
any policies are updated as required.

== Q/A ==

Create a new project, as a public information type, and then after you create
it go to the "Edit details" and change it to a Private type. It should adjust
accordingly and show as Private.

Create a new project, as a private information type, and then change it to
Public and it should.

== Tests ==

test_product.py

== Lint ==

Cleaned up a couple of lint errors from this and previous branch.

To post a comment you must log in.
Revision history for this message
Aaron Bentley (abentley) :
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/browser/tests/test_product.py'
2--- lib/lp/registry/browser/tests/test_product.py 2012-10-08 10:07:11 +0000
3+++ lib/lp/registry/browser/tests/test_product.py 2012-10-10 12:32:23 +0000
4@@ -432,6 +432,74 @@
5 cache.objects['team_membership_policy_data'])
6
7
8+class TestProductEditView(TestCaseWithFactory):
9+ """Tests for the ProductEditView"""
10+
11+ layer = DatabaseFunctionalLayer
12+
13+ def setUp(self):
14+ super(TestProductEditView, self).setUp()
15+
16+ def _make_product_edit_form(self, proprietary=False):
17+ """Return form data for product edit.
18+
19+ :param product: Factory product to base the form data base of.
20+ """
21+ if proprietary:
22+ licenses = License.OTHER_PROPRIETARY.title
23+ license_info = 'Commercial Subscription'
24+ information_type = 'PROPRIETARY'
25+ else:
26+ licenses = ['MIT']
27+ license_info = ''
28+ information_type = 'PUBLIC'
29+
30+ return {
31+ 'field.actions.change': 'Change',
32+ 'field.name': self.product.name,
33+ 'field.displayname': self.product.displayname,
34+ 'field.title': self.product.title,
35+ 'field.summary': self.product.summary,
36+ 'field.information_type': information_type,
37+ 'field.licenses': licenses,
38+ 'field.license_info': license_info,
39+ }
40+
41+ def test_change_information_type_proprietary(self):
42+ product = self.factory.makeProduct(name='fnord')
43+ with FeatureFixture({u'disclosure.private_projects.enabled': u'on'}):
44+ login_person(product.owner)
45+ form = self._make_product_edit_form(proprietary=True)
46+ view = create_initialized_view(product, '+edit', form=form)
47+ self.assertEqual(0, len(view.errors))
48+
49+ product_set = getUtility(IProductSet)
50+ updated_product = product_set.getByName('fnord')
51+ self.assertEqual(
52+ InformationType.PROPRIETARY, updated_product.information_type)
53+ # A complimentary commercial subscription is auto generated for
54+ # the product when the information type is changed.
55+ self.assertIsNotNone(updated_product.commercial_subscription)
56+
57+ def test_change_information_type_public(self):
58+ owner = self.factory.makePerson(name='pting')
59+ product = self.factory.makeProduct(
60+ name='fnord',
61+ information_type=InformationType.PROPRIETARY,
62+ owner=owner,
63+ )
64+ with FeatureFixture({u'disclosure.private_projects.enabled': u'on'}):
65+ login_person(owner)
66+ form = self._make_product_edit_form()
67+ view = create_initialized_view(product, '+edit', form=form)
68+ self.assertEqual(0, len(view.errors))
69+
70+ product_set = getUtility(IProductSet)
71+ updated_product = product_set.getByName('fnord')
72+ self.assertEqual(
73+ InformationType.PUBLIC, updated_product.information_type)
74+
75+
76 class ProductSetReviewLicensesViewTestCase(TestCaseWithFactory):
77 """Tests the ProductSetReviewLicensesView."""
78
79
80=== modified file 'lib/lp/registry/javascript/product_views.js'
81--- lib/lp/registry/javascript/product_views.js 2012-10-10 12:32:23 +0000
82+++ lib/lp/registry/javascript/product_views.js 2012-10-10 12:32:23 +0000
83@@ -345,8 +345,10 @@
84 */
85 information_type_change: function (value) {
86 toggle_license_field(value);
87- var driver_cont = Y.one('input[name="field.driver"]').ancestor('td');
88- var bug_super_cont = Y.one('input[name="field.bug_supervisor"]').ancestor('td');
89+ var driver_cont =
90+ Y.one('input[name="field.driver"]').ancestor('td');
91+ var bug_super_cont =
92+ Y.one('input[name="field.bug_supervisor"]').ancestor('td');
93
94 if (info_type.get_cache_data_from_key(value, 'value',
95 'is_private')) {
96
97=== modified file 'lib/lp/registry/model/product.py'
98--- lib/lp/registry/model/product.py 2012-10-08 10:07:11 +0000
99+++ lib/lp/registry/model/product.py 2012-10-10 12:32:23 +0000
100@@ -433,17 +433,43 @@
101 # Proprietary check works only after creation, because during
102 # creation, has_commercial_subscription cannot give the right value
103 # and triggers an inappropriate DB flush.
104- if (not self._SO_creating and value in PROPRIETARY_INFORMATION_TYPES
105- and not self.has_current_commercial_subscription):
106- raise CommercialSubscribersOnly(
107- 'A valid commercial subscription is required for private'
108- ' Projects.')
109+
110+ # If you're changing the license, and setting a PROPRIETARY
111+ # information type, yet you don't have a subscription you get one when
112+ # the license is set.
113+
114+ # If you have a commercial subscription, but it's not current, you
115+ # cannot set the information type to a PROPRIETARY type.
116+ if not self._SO_creating and value in PROPRIETARY_INFORMATION_TYPES:
117+ # Create the complimentary commercial subscription for the product.
118+ self._ensure_complimentary_subscription()
119+
120+ if not self.has_current_commercial_subscription:
121+ raise CommercialSubscribersOnly(
122+ 'A valid commercial subscription is required for private'
123+ ' Projects.')
124+
125 return value
126
127- information_type = EnumCol(
128+ _information_type = EnumCol(
129 enum=InformationType, default=InformationType.PUBLIC,
130+ dbName="information_type",
131 storm_validator=_valid_product_information_type)
132
133+ def _get_information_type(self):
134+ return self._information_type or InformationType.PUBLIC
135+
136+ def _set_information_type(self, value):
137+ self._information_type = value
138+ # Make sure that policies are updated to grant permission to the
139+ # maintainer as required for the Product.
140+ # However, only on edits. If this is a new Product it's handled
141+ # already.
142+ if not self._SO_creating:
143+ self._ensurePolicies([value])
144+
145+ information_type = property(_get_information_type, _set_information_type)
146+
147 security_contact = None
148
149 @property
150@@ -952,6 +978,15 @@
151 get_property_cache(self)._cached_licenses = tuple(sorted(licenses))
152 if (License.OTHER_PROPRIETARY in licenses
153 and self.commercial_subscription is None):
154+ self._ensure_complimentary_subscription()
155+
156+ notify(LicensesModifiedEvent(self))
157+
158+ licenses = property(_getLicenses, _setLicenses)
159+
160+ def _ensure_complimentary_subscription(self):
161+ """Create a complementary commercial subscription for the product"""
162+ if not self.commercial_subscription:
163 lp_janitor = getUtility(ILaunchpadCelebrities).janitor
164 now = datetime.datetime.now(pytz.UTC)
165 date_expires = now + datetime.timedelta(days=30)
166@@ -964,9 +999,6 @@
167 registrant=lp_janitor, purchaser=lp_janitor,
168 sales_system_id=sales_system_id, whiteboard=whiteboard)
169 get_property_cache(self).commercial_subscription = subscription
170- notify(LicensesModifiedEvent(self))
171-
172- licenses = property(_getLicenses, _setLicenses)
173
174 def _getOwner(self):
175 """Get the owner."""
176
177=== modified file 'lib/lp/registry/stories/webservice/xx-project-registry.txt'
178--- lib/lp/registry/stories/webservice/xx-project-registry.txt 2012-10-08 10:07:11 +0000
179+++ lib/lp/registry/stories/webservice/xx-project-registry.txt 2012-10-10 12:32:23 +0000
180@@ -168,7 +168,7 @@
181 freshmeat_project: None
182 homepage_url: None
183 icon_link: u'http://.../firefox/icon'
184- information_type: None
185+ information_type: u'Public'
186 is_permitted: True
187 license_approved: False
188 license_info: None
189
190=== modified file 'lib/lp/registry/tests/test_product.py'
191--- lib/lp/registry/tests/test_product.py 2012-10-10 02:45:36 +0000
192+++ lib/lp/registry/tests/test_product.py 2012-10-10 12:32:23 +0000
193@@ -4,8 +4,10 @@
194 __metaclass__ = type
195
196 from cStringIO import StringIO
197-import datetime
198-
199+from datetime import (
200+ datetime,
201+ timedelta,
202+ )
203 import pytz
204 from storm.locals import Store
205 from testtools.matchers import MatchesAll
206@@ -500,21 +502,46 @@
207 with person_logged_in(product.owner):
208 product.information_type = info_type
209
210- def test_product_information_set_proprietary_requires_commercial(self):
211- # Cannot set Product.information_type to proprietary values if no
212- # commercial subscription.
213- product = self.factory.makeProduct()
214- self.useContext(person_logged_in(product.owner))
215+ def test_set_proprietary_gets_commerical_subscription(self):
216+ # Changing a Product to Proprietary will auto generate a complimentary
217+ # subscription just as choosing a proprietary license at creation time.
218+ owner = self.factory.makePerson(name='pting')
219+ product = self.factory.makeProduct(owner=owner)
220+ self.useContext(person_logged_in(owner))
221+ self.assertIsNone(product.commercial_subscription)
222+
223+ product.information_type = InformationType.PROPRIETARY
224+ self.assertEqual(InformationType.PROPRIETARY, product.information_type)
225+ self.assertIsNotNone(product.commercial_subscription)
226+
227+ def test_set_proprietary_fails_expired_commerical_subscription(self):
228+ # Cannot set information type to proprietary with an expired
229+ # complimentary subscription.
230+ owner = self.factory.makePerson(name='pting')
231+ product = self.factory.makeProduct(
232+ information_type=InformationType.PROPRIETARY,
233+ owner=owner,
234+ )
235+ self.useContext(person_logged_in(owner))
236+
237+ # The Product now has a complimentary commercial subscription.
238+ new_expires_date = datetime.now(pytz.timezone('UTC')) - timedelta(1)
239+ naked_subscription = removeSecurityProxy(
240+ product.commercial_subscription)
241+ naked_subscription.date_expires = new_expires_date
242+
243+ # We can make the product PUBLIC
244+ product.information_type = InformationType.PUBLIC
245+ self.assertEqual(InformationType.PUBLIC, product.information_type)
246+
247+ # However we can't change it back to a Proprietary because our
248+ # commercial subscription has expired.
249 for info_type in PROPRIETARY_INFORMATION_TYPES:
250 with ExpectedException(
251 CommercialSubscribersOnly,
252 'A valid commercial subscription is required for private'
253 ' Projects.'):
254 product.information_type = info_type
255- product.redeemSubscriptionVoucher(
256- 'hello', product.owner, product.owner, 1)
257- for info_type in PROPRIETARY_INFORMATION_TYPES:
258- product.information_type = info_type
259
260 def test_product_information_init_proprietary_requires_commercial(self):
261 # Cannot create a product with proprietary types without specifying
262@@ -980,9 +1007,9 @@
263 cs = product.commercial_subscription
264 self.assertIsNotNone(cs)
265 self.assertIn('complimentary-30-day', cs.sales_system_id)
266- now = datetime.datetime.now(pytz.UTC)
267+ now = datetime.now(pytz.UTC)
268 self.assertTrue(now >= cs.date_starts)
269- future_30_days = now + datetime.timedelta(days=30)
270+ future_30_days = now + timedelta(days=30)
271 self.assertTrue(future_30_days >= cs.date_expires)
272 self.assertIn(
273 "Complimentary 30 day subscription. -- Launchpad",
274@@ -1003,9 +1030,9 @@
275 cs = product.commercial_subscription
276 self.assertIsNotNone(cs)
277 self.assertIn('complimentary-30-day', cs.sales_system_id)
278- now = datetime.datetime.now(pytz.UTC)
279+ now = datetime.now(pytz.UTC)
280 self.assertTrue(now >= cs.date_starts)
281- future_30_days = now + datetime.timedelta(days=30)
282+ future_30_days = now + timedelta(days=30)
283 self.assertTrue(future_30_days >= cs.date_expires)
284 self.assertIn(
285 "Complimentary 30 day subscription. -- Launchpad",
286@@ -1283,8 +1310,8 @@
287 product = question.product
288 transaction.commit()
289 ws_product = self.wsObject(product, product.owner)
290- now = datetime.datetime.now(tz=pytz.utc)
291- day = datetime.timedelta(days=1)
292+ now = datetime.now(tz=pytz.utc)
293+ day = timedelta(days=1)
294 self.failUnlessEqual(
295 [oopsid],
296 ws_product.findReferencedOOPS(start_date=now - day, end_date=now))
297@@ -1301,8 +1328,8 @@
298 product = self.factory.makeProduct()
299 transaction.commit()
300 ws_product = self.wsObject(product, product.owner)
301- now = datetime.datetime.now(tz=pytz.utc)
302- day = datetime.timedelta(days=1)
303+ now = datetime.now(tz=pytz.utc)
304+ day = timedelta(days=1)
305 self.failUnlessEqual(
306 [],
307 ws_product.findReferencedOOPS(start_date=now - day, end_date=now))
308
309=== modified file 'lib/lp/scripts/garbo.py'
310--- lib/lp/scripts/garbo.py 2012-10-08 10:07:11 +0000
311+++ lib/lp/scripts/garbo.py 2012-10-10 12:32:23 +0000
312@@ -928,10 +928,10 @@
313 def __call__(self, chunk_size):
314 """See `TunableLoop`."""
315 subselect = Select(
316- Product.id, Product.information_type == None,
317+ Product.id, Product._information_type == None,
318 limit=chunk_size)
319 result = self.store.execute(
320- Update({Product.information_type: 1},
321+ Update({Product._information_type: 1},
322 Product.id.is_in(subselect)))
323 transaction.commit()
324 self.rows_updated = result.rowcount