Merge lp:~sinzui/launchpad/entitlement-2 into lp:launchpad

Proposed by Curtis Hovey on 2012-03-12
Status: Superseded
Proposed branch: lp:~sinzui/launchpad/entitlement-2
Merge into: lp:launchpad
Diff against target: 747 lines (+382/-185)
8 files modified
lib/lp/registry/browser/product.py (+0/-81)
lib/lp/registry/browser/tests/test_product.py (+0/-87)
lib/lp/registry/configure.zcml (+1/-1)
lib/lp/registry/model/product.py (+16/-0)
lib/lp/registry/subscribers.py (+104/-5)
lib/lp/registry/tests/test_product.py (+97/-0)
lib/lp/registry/tests/test_subscribers.py (+152/-0)
lib/lp/testing/factory.py (+12/-11)
To merge this branch: bzr merge lp:~sinzui/launchpad/entitlement-2
Reviewer Review Type Date Requested Status
Benji York 2012-03-12 Pending
Review via email: mp+97074@code.launchpad.net

This proposal has been superseded by a proposal from 2012-03-12.

Commit Message

Move license email rules from the view to the model.

Description of the Change

We want to give proprietary projects complimentary commercial subscriptions
to ensure that projects can be configured setup to no disclose information
from the start.

This branch follows moves the code that send emails to users about
licenses from the view to the model so that changes made over the API
generate emails. Most of this branch is a refactoring.

--------------------------------------------------------------------

RULES

    PREVIOUS BRANCH
    * When a OTHER/PROPRIETARY license is added to a project, and the
      project does not have any commercial subscriptions add a commercial
      subscription that expires in 4 weeks.
      * We check for previous active and expired commercial subscriptions
        to ensure that users cannot get extra time by reconfiguring their
        project.
        * Product._setLicenses()
      * Use StevenK's example from the LaunchpadFactory to create a
        commercial subscription without a voucher.

    THIS BRANCH
    * Push the rules to send licensing emails to the model.
      * Emails are currently sent by the views that allow users to change
        licenses. Change made in the model or via the API are missin emails.
      * Notify of the ObjectModifiedEvent in the _setLicenses() method.

    FUTURE BRANCH
    * The UI and an email is sent to inform the user of the complimentary
      commercial subscription and it will explain when it expires and how
      to active the proprietary features. There is a link to purchase a
      full subscription.
      * This email probably replaces the existing email that explain Lp's
        licensing and purchasing rules.
    * At 4 weeks and 1 weeks before a commercial subscription expires, a
      reminder is sent to purchase a commercial subscription.
      * the email also explain that the project will be deactivated if the
        subscription is not renewed and the project is still proprietary
        to ensure proprietary is not added added.
      * Users can choose an Open source license. Branches and bugs will
        remain proprietary because we understand that confidential information
        can never be disclosed, but new bugs and branches will be public...
        the project's focus of development must be set to a public branch.
    * Bonus points if there is a clear way to identify a Canonical owned
      project and set the commercial subscription to expire in 10 years.

QA

    * Visit https://qastaging.launchpad.net/projects/+new and create a
      non-proprietary project.
    * Verify an email was not sent.
    * Use Change details to set the license to proprietary.
    * Verify an email was sent to the maintainer explaining Lp's licensing
      policy

    * Visit https://qastaging.launchpad.net/projects/+new and create a
      proprietary project.
    * Verify it has a commercial subscription that expires in one month.
    * Verify an email was sent to the maintainer explaining Lp's licensing
      policy

LINT

    lib/lp/registry/configure.zcml
    lib/lp/registry/subscribers.py
    lib/lp/registry/browser/product.py
    lib/lp/registry/browser/tests/test_product.py
    lib/lp/registry/model/product.py
    lib/lp/registry/tests/test_product.py
    lib/lp/registry/tests/test_subscribers.py
    lib/lp/testing/factory.py

TEST

    ./bin/test -vv lp.registry.tests.browser.test_product
    ./bin/test -vv lp.registry.tests.test_product
    ./bin/test -vv lp.registry.tests.test_subscribers

IMPLEMENTATION

Login as the product owner before creating the product so that there is an
interaction for events to use.
    lib/lp/testing/factory.py

Remove view code and tests -- moved to subscribers.py and
test_subscribers.py. The call to send an email when there were no
licenses is not needed anymore. It will never run in production because
we fixed all the production data. All products have one or more Licenses.
    lib/lp/registry/browser/product.py
    lib/lp/registry/browser/tests/test_product.py

Notify that the licenses changed.
    lib/lp/registry/model/product.py
    lib/lp/registry/tests/test_product.py

Refactored the view code and tests to work with events at the model
level. Removed a dead method called product_modified that was in
subscribers.py, I suspect it is vestigial to product._owner change event
that once needlessly changed the owner of subordinate objects.
    lib/lp/registry/configure.zcml
    lib/lp/registry/subscribers.py
    lib/lp/registry/tests/test_subscribers.py

To post a comment you must log in.

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 2012-03-08 17:08:56 +0000
3+++ lib/lp/registry/browser/product.py 2012-03-12 18:17:24 +0000
4@@ -77,7 +77,6 @@
5 SimpleTerm,
6 SimpleVocabulary,
7 )
8-from zope.security.proxy import removeSecurityProxy
9
10 from lp import _
11 from lp.answers.browser.faqtarget import FAQTargetNavigationMixin
12@@ -177,11 +176,6 @@
13 PublicPersonChoice,
14 )
15 from lp.services.librarian.interfaces import ILibraryFileAliasSet
16-from lp.services.mail.helpers import get_email_template
17-from lp.services.mail.sendmail import (
18- format_address,
19- simple_sendmail,
20- )
21 from lp.services.propertycache import cachedproperty
22 from lp.services.webapp import (
23 ApplicationMenu,
24@@ -200,7 +194,6 @@
25 from lp.services.webapp.batching import BatchNavigator
26 from lp.services.webapp.breadcrumb import Breadcrumb
27 from lp.services.webapp.interfaces import (
28- ILaunchBag,
29 UnsafeFormGetSubmissionError,
30 )
31 from lp.services.webapp.menu import NavigationMenu
32@@ -309,74 +302,6 @@
33 # Launchpad is ok with all licenses used in this project.
34 pass
35
36- def notifyCommercialMailingList(self):
37- """Notify user about Launchpad license rules."""
38- licenses = list(self.product.licenses)
39- needs_email = (
40- License.OTHER_PROPRIETARY in licenses
41- or License.OTHER_OPEN_SOURCE in licenses
42- or [License.DONT_KNOW] == licenses)
43- if not needs_email:
44- # The project has a recognized license.
45- return
46-
47- def indent(text):
48- if text is None:
49- return None
50- text = '\n '.join(line for line in text.split('\n'))
51- text = ' ' + text
52- return text
53-
54- user = getUtility(ILaunchBag).user
55- user_address = format_address(
56- user.displayname, user.preferredemail.email)
57- from_address = format_address(
58- "Launchpad", config.canonical.noreply_from_address)
59- commercial_address = format_address(
60- 'Commercial', 'commercial@launchpad.net')
61- license_titles = '\n'.join(
62- license.title for license in self.product.licenses)
63- substitutions = dict(
64- user_browsername=user.displayname,
65- user_name=user.name,
66- product_name=self.product.name,
67- product_url=canonical_url(self.product),
68- product_summary=indent(self.product.summary),
69- license_titles=indent(license_titles),
70- license_info=indent(self.product.license_info))
71- # Email the user about license policy.
72- subject = (
73- "License information for %(product_name)s "
74- "in Launchpad" % substitutions)
75- template = get_email_template(
76- 'product-other-license.txt', app='registry')
77- message = template % substitutions
78- simple_sendmail(
79- from_address, user_address,
80- subject, message, headers={'Reply-To': commercial_address})
81- # Inform that Launchpad recognized the license change.
82- self._addLicenseChangeToReviewWhiteboard()
83- self.request.response.addInfoNotification(_(
84- "Launchpad is free to use for software under approved "
85- "licenses. The Launchpad team will be in contact with "
86- "you soon."))
87-
88- def _addLicenseChangeToReviewWhiteboard(self):
89- """Update the whiteboard for the reviewer's benefit."""
90- now = self._formatDate()
91- whiteboard = 'User notified of license policy on %s.' % now
92- naked_product = removeSecurityProxy(self.product)
93- if naked_product.reviewer_whiteboard is None:
94- naked_product.reviewer_whiteboard = whiteboard
95- else:
96- naked_product.reviewer_whiteboard += '\n' + whiteboard
97-
98- def _formatDate(self, now=None):
99- """Return the date formatted for messages."""
100- if now is None:
101- now = datetime.now(tz=pytz.UTC)
102- return now.strftime('%Y-%m-%d')
103-
104
105 class ProductFacets(QuestionTargetFacetMixin, StandardLaunchpadFacets):
106 """The links that will appear in the facet menu for an IProduct."""
107@@ -1620,13 +1545,7 @@
108
109 @action("Change", name='change')
110 def change_action(self, action, data):
111- previous_licenses = self.context.licenses
112 self.updateContextFromData(data)
113- # only send email the first time licenses are set
114- if len(previous_licenses) == 0:
115- # self.product is expected by notifyCommercialMailingList
116- self.product = self.context
117- self.notifyCommercialMailingList()
118
119 @property
120 def next_url(self):
121
122=== modified file 'lib/lp/registry/browser/tests/test_product.py'
123--- lib/lp/registry/browser/tests/test_product.py 2012-03-02 06:46:29 +0000
124+++ lib/lp/registry/browser/tests/test_product.py 2012-03-12 18:17:24 +0000
125@@ -5,14 +5,9 @@
126
127 __metaclass__ = type
128
129-import datetime
130-
131-import pytz
132 from zope.component import getUtility
133-from zope.security.proxy import removeSecurityProxy
134
135 from lp.app.enums import ServiceUsage
136-from lp.registry.browser.product import ProductLicenseMixin
137 from lp.registry.interfaces.product import (
138 IProductSet,
139 License,
140@@ -22,13 +17,11 @@
141 from lp.testing import (
142 BrowserTestCase,
143 login_celebrity,
144- login_person,
145 person_logged_in,
146 TestCaseWithFactory,
147 )
148 from lp.testing.fixture import DemoMode
149 from lp.testing.layers import DatabaseFunctionalLayer
150-from lp.testing.mail_helpers import pop_notifications
151 from lp.testing.pages import find_tag_by_id
152 from lp.testing.service_usage_helpers import set_service_usage
153 from lp.testing.views import (
154@@ -37,86 +30,6 @@
155 )
156
157
158-class TestProductLicenseMixin(TestCaseWithFactory):
159-
160- layer = DatabaseFunctionalLayer
161-
162- def setUp(self):
163- # Setup an a view that implements ProductLicenseMixin.
164- super(TestProductLicenseMixin, self).setUp()
165- self.registrant = self.factory.makePerson(
166- name='registrant', email='registrant@launchpad.dev')
167- self.product = self.factory.makeProduct(
168- name='ball', owner=self.registrant)
169- self.view = create_view(self.product, '+edit')
170- self.view.product = self.product
171- login_person(self.registrant)
172-
173- def verify_whiteboard(self):
174- # Verify that the review whiteboard was updated.
175- naked_product = removeSecurityProxy(self.product)
176- whiteboard, stamp = naked_product.reviewer_whiteboard.rsplit(' ', 1)
177- self.assertEqual(
178- 'User notified of license policy on', whiteboard)
179-
180- def verify_user_email(self, notification):
181- # Verify that the user was sent an email about the license change.
182- self.assertEqual(
183- 'License information for ball in Launchpad',
184- notification['Subject'])
185- self.assertEqual(
186- 'Registrant <registrant@launchpad.dev>',
187- notification['To'])
188- self.assertEqual(
189- 'Commercial <commercial@launchpad.net>',
190- notification['Reply-To'])
191-
192- def test_ProductLicenseMixin_instance(self):
193- # The object under test is an instance of ProductLicenseMixin.
194- self.assertTrue(isinstance(self.view, ProductLicenseMixin))
195-
196- def test_notifyCommercialMailingList_known_license(self):
197- # A known license does not generate an email.
198- self.product.licenses = [License.GNU_GPL_V2]
199- self.view.notifyCommercialMailingList()
200- self.assertEqual(0, len(pop_notifications()))
201-
202- def test_notifyCommercialMailingList_other_dont_know(self):
203- # An Other/I don't know license sends one email.
204- self.product.licenses = [License.DONT_KNOW]
205- self.view.notifyCommercialMailingList()
206- self.verify_whiteboard()
207- notifications = pop_notifications()
208- self.assertEqual(1, len(notifications))
209- self.verify_user_email(notifications.pop())
210-
211- def test_notifyCommercialMailingList_other_open_source(self):
212- # An Other/Open Source license sends one email.
213- self.product.licenses = [License.OTHER_OPEN_SOURCE]
214- self.product.license_info = 'http://www,boost.org/'
215- self.view.notifyCommercialMailingList()
216- self.verify_whiteboard()
217- notifications = pop_notifications()
218- self.assertEqual(1, len(notifications))
219- self.verify_user_email(notifications.pop())
220-
221- def test_notifyCommercialMailingList_other_proprietary(self):
222- # An Other/Proprietary license sends one email.
223- self.product.licenses = [License.OTHER_PROPRIETARY]
224- self.product.license_info = 'All mine'
225- self.view.notifyCommercialMailingList()
226- self.verify_whiteboard()
227- notifications = pop_notifications()
228- self.assertEqual(1, len(notifications))
229- self.verify_user_email(notifications.pop())
230-
231- def test__formatDate(self):
232- # Verify the date format.
233- now = datetime.datetime(2005, 6, 15, 0, 0, 0, 0, pytz.UTC)
234- result = self.view._formatDate(now)
235- self.assertEqual('2005-06-15', result)
236-
237-
238 class TestProductConfiguration(TestCaseWithFactory):
239 """Tests the configuration links and helpers."""
240
241
242=== modified file 'lib/lp/registry/configure.zcml'
243--- lib/lp/registry/configure.zcml 2012-02-24 07:17:43 +0000
244+++ lib/lp/registry/configure.zcml 2012-03-12 18:17:24 +0000
245@@ -1056,7 +1056,7 @@
246 </class>
247 <subscriber
248 for="lp.registry.interfaces.product.IProduct zope.lifecycleevent.interfaces.IObjectModifiedEvent"
249- handler="lp.registry.subscribers.product_modified"/>
250+ handler="lp.registry.subscribers.product_licenses_modified"/>
251 <class
252 class="lp.registry.model.mailinglist.MailingList">
253 <allow
254
255=== modified file 'lib/lp/registry/model/product.py'
256--- lib/lp/registry/model/product.py 2012-03-12 11:02:48 +0000
257+++ lib/lp/registry/model/product.py 2012-03-12 18:17:24 +0000
258@@ -774,6 +774,22 @@
259 for license in licenses.difference(old_licenses):
260 ProductLicense(product=self, license=license)
261 get_property_cache(self)._cached_licenses = tuple(sorted(licenses))
262+ if (License.OTHER_PROPRIETARY in licenses
263+ and self.commercial_subscription is None):
264+ lp_janitor = getUtility(ILaunchpadCelebrities).janitor
265+ now = datetime.datetime.now(pytz.UTC)
266+ date_expires = now + datetime.timedelta(days=30)
267+ sales_system_id = 'complimentary-30-day-%s' % now
268+ whiteboard = (
269+ "Complimentary 30 day subscription. -- Launchpad %s" %
270+ now.date().isoformat())
271+ subscription = CommercialSubscription(
272+ product=self, date_starts=now, date_expires=date_expires,
273+ registrant=lp_janitor, purchaser=lp_janitor,
274+ sales_system_id=sales_system_id, whiteboard=whiteboard)
275+ get_property_cache(self).commercial_subscription = subscription
276+ # Do not use a snapshot because the past is unintersting.
277+ notify(ObjectModifiedEvent(self, self, edited_fields=['licenses']))
278
279 licenses = property(_getLicenses, _setLicenses)
280
281
282=== modified file 'lib/lp/registry/subscribers.py'
283--- lib/lp/registry/subscribers.py 2009-06-25 04:06:00 +0000
284+++ lib/lp/registry/subscribers.py 2012-03-12 18:17:24 +0000
285@@ -1,13 +1,112 @@
286 # Copyright 2009 Canonical Ltd. This software is licensed under the
287 # GNU Affero General Public License version 3 (see the file LICENSE).
288+"""Functions and classes that are subscribed to registry events."""
289
290 __metaclass__ = type
291
292 __all__ = [
293- 'product_modified',
294+ 'product_licenses_modified',
295 ]
296
297-
298-def product_modified(product, event):
299- pass
300-
301+from datetime import datetime
302+
303+import pytz
304+
305+from zope.security.proxy import removeSecurityProxy
306+
307+from lp.registry.interfaces.person import IPerson
308+from lp.registry.interfaces.product import License
309+from lp.services.config import config
310+from lp.services.mail.helpers import get_email_template
311+from lp.services.mail.sendmail import (
312+ format_address,
313+ simple_sendmail,
314+ )
315+from lp.services.webapp.publisher import canonical_url
316+
317+
318+def product_licenses_modified(product, event):
319+ """Send a notification if licenses changed and a license is special."""
320+ licenses_changed = 'licenses' in event.edited_fields
321+ needs_notification = LicenseNotification.needs_notification(product)
322+ if licenses_changed and needs_notification:
323+ user = IPerson(event.user)
324+ notification = LicenseNotification(product, user)
325+ notification.send()
326+
327+
328+class LicenseNotification:
329+ """Send notification about special licenses to the user."""
330+
331+ def __init__(self, product, user):
332+ self.product = product
333+ self.user = user
334+
335+ @staticmethod
336+ def needs_notification(product):
337+ licenses = list(product.licenses)
338+ return (
339+ License.OTHER_PROPRIETARY in licenses
340+ or License.OTHER_OPEN_SOURCE in licenses
341+ or [License.DONT_KNOW] == licenses)
342+
343+ def send(self):
344+ """Send a message to the user about the product's license."""
345+ if not self.needs_notification(self.product):
346+ # The project has a common license.
347+ return False
348+ user_address = format_address(
349+ self.user.displayname, self.user.preferredemail.email)
350+ from_address = format_address(
351+ "Launchpad", config.canonical.noreply_from_address)
352+ commercial_address = format_address(
353+ 'Commercial', 'commercial@launchpad.net')
354+ license_titles = '\n'.join(
355+ license.title for license in self.product.licenses)
356+ substitutions = dict(
357+ user_browsername=self.user.displayname,
358+ user_name=self.user.name,
359+ product_name=self.product.name,
360+ product_url=canonical_url(self.product),
361+ product_summary=self._indent(self.product.summary),
362+ license_titles=self._indent(license_titles),
363+ license_info=self._indent(self.product.license_info))
364+ # Email the user about license policy.
365+ subject = (
366+ "License information for %(product_name)s "
367+ "in Launchpad" % substitutions)
368+ template = get_email_template(
369+ 'product-other-license.txt', app='registry')
370+ message = template % substitutions
371+ simple_sendmail(
372+ from_address, user_address,
373+ subject, message, headers={'Reply-To': commercial_address})
374+ # Inform that Launchpad recognized the license change.
375+ self._addLicenseChangeToReviewWhiteboard()
376+ return True
377+
378+ @staticmethod
379+ def _indent(text):
380+ """Indent the text to be included in the message."""
381+ if text is None:
382+ return None
383+ text = '\n '.join(line for line in text.split('\n'))
384+ text = ' ' + text
385+ return text
386+
387+ @staticmethod
388+ def _formatDate(now=None):
389+ """Return the date formatted for messages."""
390+ if now is None:
391+ now = datetime.now(tz=pytz.UTC)
392+ return now.strftime('%Y-%m-%d')
393+
394+ def _addLicenseChangeToReviewWhiteboard(self):
395+ """Update the whiteboard for the reviewer's benefit."""
396+ now = self._formatDate()
397+ whiteboard = 'User notified of license policy on %s.' % now
398+ naked_product = removeSecurityProxy(self.product)
399+ if naked_product.reviewer_whiteboard is None:
400+ naked_product.reviewer_whiteboard = whiteboard
401+ else:
402+ naked_product.reviewer_whiteboard += '\n' + whiteboard
403
404=== modified file 'lib/lp/registry/tests/test_product.py'
405--- lib/lp/registry/tests/test_product.py 2012-03-12 11:02:48 +0000
406+++ lib/lp/registry/tests/test_product.py 2012-03-12 18:17:24 +0000
407@@ -11,6 +11,7 @@
408 from testtools.matchers import MatchesAll
409 import transaction
410 from zope.component import getUtility
411+from zope.lifecycleevent.interfaces import IObjectModifiedEvent
412 from zope.security.interfaces import Unauthorized
413 from zope.security.proxy import removeSecurityProxy
414
415@@ -20,6 +21,7 @@
416 IHasIcon,
417 IHasLogo,
418 IHasMugshot,
419+ ILaunchpadCelebrities,
420 ILaunchpadUsage,
421 IServiceUsage,
422 )
423@@ -38,6 +40,7 @@
424 )
425 from lp.registry.interfaces.product import (
426 IProduct,
427+ IProductSet,
428 License,
429 )
430 from lp.registry.interfaces.series import SeriesStatus
431@@ -55,6 +58,7 @@
432 TestCaseWithFactory,
433 WebServiceTestCase,
434 )
435+from lp.testing.event import TestEventListener
436 from lp.testing.layers import (
437 DatabaseFunctionalLayer,
438 LaunchpadFunctionalLayer,
439@@ -459,6 +463,19 @@
440 """Test the rules of licenses and commercial subscriptions."""
441
442 layer = DatabaseFunctionalLayer
443+ event_listener = None
444+
445+ def setup_event_listener(self):
446+ self.events = []
447+ if self.event_listener is None:
448+ self.event_listener = TestEventListener(
449+ IProduct, IObjectModifiedEvent, self.on_event)
450+ else:
451+ self.event_listener._active = True
452+ self.addCleanup(self.event_listener.unregister)
453+
454+ def on_event(self, thing, event):
455+ self.events.append(event)
456
457 def test_getLicenses(self):
458 # License are assigned a list, but return a tuple.
459@@ -472,10 +489,24 @@
460 product = self.factory.makeProduct(licenses=[License.MIT])
461 with celebrity_logged_in('registry_experts'):
462 product.project_reviewed = True
463+ self.setup_event_listener()
464 with person_logged_in(product.owner):
465 product.licenses = [License.MIT]
466 with celebrity_logged_in('registry_experts'):
467 self.assertIs(True, product.project_reviewed)
468+ self.assertEqual([], self.events)
469+
470+ def test_setLicense(self):
471+ # The project_reviewed property is not reset, if the new licenses
472+ # are identical to the current licenses.
473+ product = self.factory.makeProduct()
474+ self.setup_event_listener()
475+ with person_logged_in(product.owner):
476+ product.licenses = [License.MIT]
477+ self.assertEqual((License.MIT, ), product.licenses)
478+ self.assertEqual(1, len(self.events))
479+ self.assertEqual(product, self.events[0].object)
480+ self.assertEqual(['licenses'], self.events[0].edited_fields)
481
482 def test_setLicense_also_sets_reviewed(self):
483 # The project_reviewed attribute it set to False if the licenses
484@@ -514,6 +545,72 @@
485 self.assertRaises(
486 ValueError, setattr, product, 'licenses', ['bogus'])
487
488+ def test_setLicense_non_proprietary(self):
489+ # Non-proprietary projects are not given a complimentary
490+ # commercial subscription.
491+ product = self.factory.makeProduct(licenses=[License.MIT])
492+ self.assertIsNone(product.commercial_subscription)
493+
494+ def test_setLicense_proprietary_with_commercial_subscription(self):
495+ # Proprietary projects with existing commercial subscriptions are not
496+ # given a complimentary commercial subscription.
497+ product = self.factory.makeProduct()
498+ self.factory.makeCommercialSubscription(product)
499+ with celebrity_logged_in('admin'):
500+ product.commercial_subscription.sales_system_id = 'testing'
501+ date_expires = product.commercial_subscription.date_expires
502+ with person_logged_in(product.owner):
503+ product.licenses = [License.OTHER_PROPRIETARY]
504+ with celebrity_logged_in('admin'):
505+ self.assertEqual(
506+ 'testing', product.commercial_subscription.sales_system_id)
507+ self.assertEqual(
508+ date_expires, product.commercial_subscription.date_expires)
509+
510+ def test_setLicense_proprietary_without_commercial_subscription(self):
511+ # Proprietary projects without a commercial subscriptions are
512+ # given a complimentary 30 day commercial subscription.
513+ product = self.factory.makeProduct()
514+ with person_logged_in(product.owner):
515+ product.licenses = [License.OTHER_PROPRIETARY]
516+ with celebrity_logged_in('admin'):
517+ cs = product.commercial_subscription
518+ self.assertIsNotNone(cs)
519+ self.assertIn('complimentary-30-day', cs.sales_system_id)
520+ now = datetime.datetime.now(pytz.UTC)
521+ self.assertTrue(now >= cs.date_starts)
522+ future_30_days = now + datetime.timedelta(days=30)
523+ self.assertTrue(future_30_days >= cs.date_expires)
524+ self.assertIn(
525+ "Complimentary 30 day subscription. -- Launchpad",
526+ cs.whiteboard)
527+ lp_janitor = getUtility(ILaunchpadCelebrities).janitor
528+ self.assertEqual(lp_janitor, cs.registrant)
529+ self.assertEqual(lp_janitor, cs.purchaser)
530+
531+ def test_new_proprietary_has_commercial_subscription(self):
532+ # New proprietary projects are given a complimentary 30 day
533+ # commercial subscription.
534+ owner = self.factory.makePerson()
535+ with person_logged_in(owner):
536+ product = getUtility(IProductSet).createProduct(
537+ owner, 'fnord', 'Fnord', 'Fnord', 'test 1', 'test 2',
538+ licenses=[License.OTHER_PROPRIETARY])
539+ with celebrity_logged_in('admin'):
540+ cs = product.commercial_subscription
541+ self.assertIsNotNone(cs)
542+ self.assertIn('complimentary-30-day', cs.sales_system_id)
543+ now = datetime.datetime.now(pytz.UTC)
544+ self.assertTrue(now >= cs.date_starts)
545+ future_30_days = now + datetime.timedelta(days=30)
546+ self.assertTrue(future_30_days >= cs.date_expires)
547+ self.assertIn(
548+ "Complimentary 30 day subscription. -- Launchpad",
549+ cs.whiteboard)
550+ lp_janitor = getUtility(ILaunchpadCelebrities).janitor
551+ self.assertEqual(lp_janitor, cs.registrant)
552+ self.assertEqual(lp_janitor, cs.purchaser)
553+
554
555 class ProductSnapshotTestCase(TestCaseWithFactory):
556 """Test product snapshots.
557
558=== added file 'lib/lp/registry/tests/test_subscribers.py'
559--- lib/lp/registry/tests/test_subscribers.py 1970-01-01 00:00:00 +0000
560+++ lib/lp/registry/tests/test_subscribers.py 2012-03-12 18:17:24 +0000
561@@ -0,0 +1,152 @@
562+# Copyright 2012 Canonical Ltd. This software is licensed under the
563+# GNU Affero General Public License version 3 (see the file LICENSE).
564+
565+"""Test subscruber classes and functions."""
566+
567+__metaclass__ = type
568+
569+from datetime import datetime
570+
571+import pytz
572+
573+from zope.security.proxy import removeSecurityProxy
574+from lazr.lifecycle.event import ObjectModifiedEvent
575+
576+from lp.registry.interfaces.product import License
577+from lp.registry.subscribers import (
578+ LicenseNotification,
579+ product_licenses_modified,
580+ )
581+from lp.testing import (
582+ login_person,
583+ TestCaseWithFactory,
584+ )
585+from lp.testing.layers import DatabaseFunctionalLayer
586+from lp.testing.mail_helpers import pop_notifications
587+
588+
589+class ProductLicensesModifiedTestCase(TestCaseWithFactory):
590+
591+ layer = DatabaseFunctionalLayer
592+
593+ def make_product_event(self, licenses, edited_fields='licenses'):
594+ product = self.factory.makeProduct(licenses=licenses)
595+ pop_notifications()
596+ login_person(product.owner)
597+ event = ObjectModifiedEvent(
598+ product, product, edited_fields, user=product.owner)
599+ return product, event
600+
601+ def test_product_licenses_modified_licenses_not_edited(self):
602+ product, event = self.make_product_event(
603+ [License.OTHER_PROPRIETARY], edited_fields='_owner')
604+ product_licenses_modified(product, event)
605+ notifications = pop_notifications()
606+ self.assertEqual(0, len(notifications))
607+
608+ def test_product_licenses_modified_licenses_common_license(self):
609+ product, event = self.make_product_event([License.MIT])
610+ product_licenses_modified(product, event)
611+ notifications = pop_notifications()
612+ self.assertEqual(0, len(notifications))
613+
614+ def test_product_licenses_modified_licenses_other_proprietary(self):
615+ product, event = self.make_product_event([License.OTHER_PROPRIETARY])
616+ product_licenses_modified(product, event)
617+ notifications = pop_notifications()
618+ self.assertEqual(1, len(notifications))
619+
620+ def test_product_licenses_modified_licenses_other_open_source(self):
621+ product, event = self.make_product_event([License.OTHER_OPEN_SOURCE])
622+ product_licenses_modified(product, event)
623+ notifications = pop_notifications()
624+ self.assertEqual(1, len(notifications))
625+
626+ def test_product_licenses_modified_licenses_other_dont_know(self):
627+ product, event = self.make_product_event([License.DONT_KNOW])
628+ product_licenses_modified(product, event)
629+ notifications = pop_notifications()
630+ self.assertEqual(1, len(notifications))
631+
632+
633+class LicenseNotificationTestCase(TestCaseWithFactory):
634+
635+ layer = DatabaseFunctionalLayer
636+
637+ def make_product_user(self, licenses):
638+ # Setup an a view that implements ProductLicenseMixin.
639+ super(LicenseNotificationTestCase, self).setUp()
640+ user = self.factory.makePerson(
641+ name='registrant', email='registrant@launchpad.dev')
642+ login_person(user)
643+ product = self.factory.makeProduct(
644+ name='ball', owner=user, licenses=licenses)
645+ pop_notifications()
646+ return product, user
647+
648+ def verify_whiteboard(self, product):
649+ # Verify that the review whiteboard was updated.
650+ naked_product = removeSecurityProxy(product)
651+ entries = naked_product.reviewer_whiteboard.split('\n')
652+ whiteboard, stamp = entries[-1].rsplit(' ', 1)
653+ self.assertEqual(
654+ 'User notified of license policy on', whiteboard)
655+
656+ def verify_user_email(self, notification):
657+ # Verify that the user was sent an email about the license change.
658+ self.assertEqual(
659+ 'License information for ball in Launchpad',
660+ notification['Subject'])
661+ self.assertEqual(
662+ 'Registrant <registrant@launchpad.dev>',
663+ notification['To'])
664+ self.assertEqual(
665+ 'Commercial <commercial@launchpad.net>',
666+ notification['Reply-To'])
667+
668+ def test_notifyCommercialMailingList_known_license(self):
669+ # A known license does not generate an email.
670+ product, user = self.make_product_user([License.GNU_GPL_V2])
671+ notification = LicenseNotification(product, user)
672+ result = notification.send()
673+ self.assertIs(False, result)
674+ self.assertEqual(0, len(pop_notifications()))
675+
676+ def test_notifyCommercialMailingList_other_dont_know(self):
677+ # An Other/I don't know license sends one email.
678+ product, user = self.make_product_user([License.DONT_KNOW])
679+ notification = LicenseNotification(product, user)
680+ result = notification.send()
681+ self.assertIs(True, result)
682+ self.verify_whiteboard(product)
683+ notifications = pop_notifications()
684+ self.assertEqual(1, len(notifications))
685+ self.verify_user_email(notifications.pop())
686+
687+ def test_notifyCommercialMailingList_other_open_source(self):
688+ # An Other/Open Source license sends one email.
689+ product, user = self.make_product_user([License.OTHER_OPEN_SOURCE])
690+ notification = LicenseNotification(product, user)
691+ result = notification.send()
692+ self.assertIs(True, result)
693+ self.verify_whiteboard(product)
694+ notifications = pop_notifications()
695+ self.assertEqual(1, len(notifications))
696+ self.verify_user_email(notifications.pop())
697+
698+ def test_notifyCommercialMailingList_other_proprietary(self):
699+ # An Other/Proprietary license sends one email.
700+ product, user = self.make_product_user([License.OTHER_PROPRIETARY])
701+ notification = LicenseNotification(product, user)
702+ result = notification.send()
703+ self.assertIs(True, result)
704+ self.verify_whiteboard(product)
705+ notifications = pop_notifications()
706+ self.assertEqual(1, len(notifications))
707+ self.verify_user_email(notifications.pop())
708+
709+ def test_formatDate(self):
710+ # Verify the date format.
711+ now = datetime(2005, 6, 15, 0, 0, 0, 0, pytz.UTC)
712+ result = LicenseNotification._formatDate(now)
713+ self.assertEqual('2005-06-15', result)
714
715=== modified file 'lib/lp/testing/factory.py'
716--- lib/lp/testing/factory.py 2012-03-07 00:23:37 +0000
717+++ lib/lp/testing/factory.py 2012-03-12 18:17:24 +0000
718@@ -967,17 +967,18 @@
719 title = self.getUniqueString('title')
720 if summary is None:
721 summary = self.getUniqueString('summary')
722- product = getUtility(IProductSet).createProduct(
723- owner,
724- name,
725- displayname,
726- title,
727- summary,
728- self.getUniqueString('description'),
729- licenses=licenses,
730- project=project,
731- registrant=registrant,
732- icon=icon)
733+ with person_logged_in(owner):
734+ product = getUtility(IProductSet).createProduct(
735+ owner,
736+ name,
737+ displayname,
738+ title,
739+ summary,
740+ self.getUniqueString('description'),
741+ licenses=licenses,
742+ project=project,
743+ registrant=registrant,
744+ icon=icon)
745 naked_product = removeSecurityProxy(product)
746 if official_malone is not None:
747 naked_product.official_malone = official_malone