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

Proposed by Curtis Hovey
Status: Merged
Approved by: Curtis Hovey
Approved revision: no longer in the source branch.
Merged at revision: 14953
Proposed branch: lp:~sinzui/launchpad/entitlement-2
Merge into: lp:launchpad
Prerequisite: lp:~sinzui/launchpad/entitlement-1
Diff against target: 685 lines (+309/-189)
9 files modified
lib/lp/registry/browser/product.py (+0/-82)
lib/lp/registry/browser/tests/test_product.py (+0/-87)
lib/lp/registry/configure.zcml (+1/-1)
lib/lp/registry/model/product.py (+2/-0)
lib/lp/registry/stories/product/xx-product-index.txt (+2/-0)
lib/lp/registry/subscribers.py (+106/-5)
lib/lp/registry/tests/test_product.py (+33/-3)
lib/lp/registry/tests/test_subscribers.py (+152/-0)
lib/lp/testing/factory.py (+13/-11)
To merge this branch: bzr merge lp:~sinzui/launchpad/entitlement-2
Reviewer Review Type Date Requested Status
Benji York (community) code Approve
Review via email: mp+97078@code.launchpad.net

This proposal supersedes a proposal from 2012-03-12.

Commit message

Move license notification code 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.
Revision history for this message
Benji York (benji) wrote :

This branch looks good, the only comments I have are concerning code
that hasn't actually changed, so take that into consideration:

Is it possible that the strings included in the body of the email might
be wider than is customary for email messages? If so, we can use the
textwrap.fill function to tame the long lines.

It's arguable as to whether this is an improvement or not, but it
occurred to me that _indent() could be implemented with this regex:

    re.sub('^', ' '*4, text, flags=re.M)

Using the string "Commercial" as the display-name portion of the from
address seems a little vague. Maybe something like "Launchpad
Commercial Services" (or a shorter variation) would be nicer.

review: Approve (code)
Revision history for this message
Curtis Hovey (sinzui) wrote :

Thank you for suggestion. I will look into this in my next branch which introduces new information into the email to explain complimentary subscriptions and how to configure privacy.

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-15 14:59:36 +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@@ -2284,7 +2203,6 @@
122 def main_action(self, data):
123 """See `MultiStepView`."""
124 self.product = self.create_product(data)
125- self.notifyCommercialMailingList()
126 notify(ObjectCreatedEvent(self.product))
127 self.link_source_package(self.product, data)
128
129
130=== modified file 'lib/lp/registry/browser/tests/test_product.py'
131--- lib/lp/registry/browser/tests/test_product.py 2012-03-02 06:46:29 +0000
132+++ lib/lp/registry/browser/tests/test_product.py 2012-03-15 14:59:36 +0000
133@@ -5,14 +5,9 @@
134
135 __metaclass__ = type
136
137-import datetime
138-
139-import pytz
140 from zope.component import getUtility
141-from zope.security.proxy import removeSecurityProxy
142
143 from lp.app.enums import ServiceUsage
144-from lp.registry.browser.product import ProductLicenseMixin
145 from lp.registry.interfaces.product import (
146 IProductSet,
147 License,
148@@ -22,13 +17,11 @@
149 from lp.testing import (
150 BrowserTestCase,
151 login_celebrity,
152- login_person,
153 person_logged_in,
154 TestCaseWithFactory,
155 )
156 from lp.testing.fixture import DemoMode
157 from lp.testing.layers import DatabaseFunctionalLayer
158-from lp.testing.mail_helpers import pop_notifications
159 from lp.testing.pages import find_tag_by_id
160 from lp.testing.service_usage_helpers import set_service_usage
161 from lp.testing.views import (
162@@ -37,86 +30,6 @@
163 )
164
165
166-class TestProductLicenseMixin(TestCaseWithFactory):
167-
168- layer = DatabaseFunctionalLayer
169-
170- def setUp(self):
171- # Setup an a view that implements ProductLicenseMixin.
172- super(TestProductLicenseMixin, self).setUp()
173- self.registrant = self.factory.makePerson(
174- name='registrant', email='registrant@launchpad.dev')
175- self.product = self.factory.makeProduct(
176- name='ball', owner=self.registrant)
177- self.view = create_view(self.product, '+edit')
178- self.view.product = self.product
179- login_person(self.registrant)
180-
181- def verify_whiteboard(self):
182- # Verify that the review whiteboard was updated.
183- naked_product = removeSecurityProxy(self.product)
184- whiteboard, stamp = naked_product.reviewer_whiteboard.rsplit(' ', 1)
185- self.assertEqual(
186- 'User notified of license policy on', whiteboard)
187-
188- def verify_user_email(self, notification):
189- # Verify that the user was sent an email about the license change.
190- self.assertEqual(
191- 'License information for ball in Launchpad',
192- notification['Subject'])
193- self.assertEqual(
194- 'Registrant <registrant@launchpad.dev>',
195- notification['To'])
196- self.assertEqual(
197- 'Commercial <commercial@launchpad.net>',
198- notification['Reply-To'])
199-
200- def test_ProductLicenseMixin_instance(self):
201- # The object under test is an instance of ProductLicenseMixin.
202- self.assertTrue(isinstance(self.view, ProductLicenseMixin))
203-
204- def test_notifyCommercialMailingList_known_license(self):
205- # A known license does not generate an email.
206- self.product.licenses = [License.GNU_GPL_V2]
207- self.view.notifyCommercialMailingList()
208- self.assertEqual(0, len(pop_notifications()))
209-
210- def test_notifyCommercialMailingList_other_dont_know(self):
211- # An Other/I don't know license sends one email.
212- self.product.licenses = [License.DONT_KNOW]
213- self.view.notifyCommercialMailingList()
214- self.verify_whiteboard()
215- notifications = pop_notifications()
216- self.assertEqual(1, len(notifications))
217- self.verify_user_email(notifications.pop())
218-
219- def test_notifyCommercialMailingList_other_open_source(self):
220- # An Other/Open Source license sends one email.
221- self.product.licenses = [License.OTHER_OPEN_SOURCE]
222- self.product.license_info = 'http://www,boost.org/'
223- self.view.notifyCommercialMailingList()
224- self.verify_whiteboard()
225- notifications = pop_notifications()
226- self.assertEqual(1, len(notifications))
227- self.verify_user_email(notifications.pop())
228-
229- def test_notifyCommercialMailingList_other_proprietary(self):
230- # An Other/Proprietary license sends one email.
231- self.product.licenses = [License.OTHER_PROPRIETARY]
232- self.product.license_info = 'All mine'
233- self.view.notifyCommercialMailingList()
234- self.verify_whiteboard()
235- notifications = pop_notifications()
236- self.assertEqual(1, len(notifications))
237- self.verify_user_email(notifications.pop())
238-
239- def test__formatDate(self):
240- # Verify the date format.
241- now = datetime.datetime(2005, 6, 15, 0, 0, 0, 0, pytz.UTC)
242- result = self.view._formatDate(now)
243- self.assertEqual('2005-06-15', result)
244-
245-
246 class TestProductConfiguration(TestCaseWithFactory):
247 """Tests the configuration links and helpers."""
248
249
250=== modified file 'lib/lp/registry/configure.zcml'
251--- lib/lp/registry/configure.zcml 2012-02-24 07:17:43 +0000
252+++ lib/lp/registry/configure.zcml 2012-03-15 14:59:36 +0000
253@@ -1056,7 +1056,7 @@
254 </class>
255 <subscriber
256 for="lp.registry.interfaces.product.IProduct zope.lifecycleevent.interfaces.IObjectModifiedEvent"
257- handler="lp.registry.subscribers.product_modified"/>
258+ handler="lp.registry.subscribers.product_licenses_modified"/>
259 <class
260 class="lp.registry.model.mailinglist.MailingList">
261 <allow
262
263=== modified file 'lib/lp/registry/model/product.py'
264--- lib/lp/registry/model/product.py 2012-03-15 14:59:35 +0000
265+++ lib/lp/registry/model/product.py 2012-03-15 14:59:36 +0000
266@@ -788,6 +788,8 @@
267 registrant=lp_janitor, purchaser=lp_janitor,
268 sales_system_id=sales_system_id, whiteboard=whiteboard)
269 get_property_cache(self).commercial_subscription = subscription
270+ # Do not use a snapshot because the past is unintersting.
271+ notify(ObjectModifiedEvent(self, self, edited_fields=['licenses']))
272
273 licenses = property(_getLicenses, _setLicenses)
274
275
276=== modified file 'lib/lp/registry/stories/product/xx-product-index.txt'
277--- lib/lp/registry/stories/product/xx-product-index.txt 2011-12-30 06:14:56 +0000
278+++ lib/lp/registry/stories/product/xx-product-index.txt 2012-03-15 14:59:36 +0000
279@@ -192,9 +192,11 @@
280 much time left on its commercial subscription, a portlet is displayed to
281 direct the owner to purchase a subscription.
282
283+ >>> login_person(firefox.owner)
284 >>> firefox.licenses = [License.OTHER_PROPRIETARY]
285 >>> flush_database_updates()
286 >>> transaction.commit()
287+ >>> logout()
288 >>> owner_browser.open('http://launchpad.dev/firefox')
289 >>> print find_tag_by_id(owner_browser.contents, 'license-status')
290 <...This project&rsquo;s license is proprietary...
291
292=== modified file 'lib/lp/registry/subscribers.py'
293--- lib/lp/registry/subscribers.py 2009-06-25 04:06:00 +0000
294+++ lib/lp/registry/subscribers.py 2012-03-15 14:59:36 +0000
295@@ -1,13 +1,114 @@
296 # Copyright 2009 Canonical Ltd. This software is licensed under the
297 # GNU Affero General Public License version 3 (see the file LICENSE).
298+"""Functions and classes that are subscribed to registry events."""
299
300 __metaclass__ = type
301
302 __all__ = [
303- 'product_modified',
304+ 'product_licenses_modified',
305 ]
306
307-
308-def product_modified(product, event):
309- pass
310-
311+from datetime import datetime
312+
313+import pytz
314+
315+from zope.security.proxy import removeSecurityProxy
316+
317+from lp.registry.interfaces.person import IPerson
318+from lp.registry.interfaces.product import License
319+from lp.services.config import config
320+from lp.services.mail.helpers import get_email_template
321+from lp.services.mail.sendmail import (
322+ format_address,
323+ simple_sendmail,
324+ )
325+from lp.services.webapp.publisher import canonical_url
326+
327+
328+def product_licenses_modified(product, event):
329+ """Send a notification if licenses changed and a license is special."""
330+ if not event.edited_fields:
331+ return
332+ licenses_changed = 'licenses' in event.edited_fields
333+ needs_notification = LicenseNotification.needs_notification(product)
334+ if licenses_changed and needs_notification:
335+ user = IPerson(event.user)
336+ notification = LicenseNotification(product, user)
337+ notification.send()
338+
339+
340+class LicenseNotification:
341+ """Send notification about special licenses to the user."""
342+
343+ def __init__(self, product, user):
344+ self.product = product
345+ self.user = user
346+
347+ @staticmethod
348+ def needs_notification(product):
349+ licenses = list(product.licenses)
350+ return (
351+ License.OTHER_PROPRIETARY in licenses
352+ or License.OTHER_OPEN_SOURCE in licenses
353+ or [License.DONT_KNOW] == licenses)
354+
355+ def send(self):
356+ """Send a message to the user about the product's license."""
357+ if not self.needs_notification(self.product):
358+ # The project has a common license.
359+ return False
360+ user_address = format_address(
361+ self.user.displayname, self.user.preferredemail.email)
362+ from_address = format_address(
363+ "Launchpad", config.canonical.noreply_from_address)
364+ commercial_address = format_address(
365+ 'Commercial', 'commercial@launchpad.net')
366+ license_titles = '\n'.join(
367+ license.title for license in self.product.licenses)
368+ substitutions = dict(
369+ user_browsername=self.user.displayname,
370+ user_name=self.user.name,
371+ product_name=self.product.name,
372+ product_url=canonical_url(self.product),
373+ product_summary=self._indent(self.product.summary),
374+ license_titles=self._indent(license_titles),
375+ license_info=self._indent(self.product.license_info))
376+ # Email the user about license policy.
377+ subject = (
378+ "License information for %(product_name)s "
379+ "in Launchpad" % substitutions)
380+ template = get_email_template(
381+ 'product-other-license.txt', app='registry')
382+ message = template % substitutions
383+ simple_sendmail(
384+ from_address, user_address,
385+ subject, message, headers={'Reply-To': commercial_address})
386+ # Inform that Launchpad recognized the license change.
387+ self._addLicenseChangeToReviewWhiteboard()
388+ return True
389+
390+ @staticmethod
391+ def _indent(text):
392+ """Indent the text to be included in the message."""
393+ if text is None:
394+ return None
395+ text = '\n '.join(line for line in text.split('\n'))
396+ text = ' ' + text
397+ return text
398+
399+ @staticmethod
400+ def _formatDate(now=None):
401+ """Return the date formatted for messages."""
402+ if now is None:
403+ now = datetime.now(tz=pytz.UTC)
404+ return now.strftime('%Y-%m-%d')
405+
406+ def _addLicenseChangeToReviewWhiteboard(self):
407+ """Update the whiteboard for the reviewer's benefit."""
408+ now = self._formatDate()
409+ whiteboard = 'User notified of license policy on %s.' % now
410+ naked_product = removeSecurityProxy(self.product)
411+ if naked_product.reviewer_whiteboard is None:
412+ naked_product.reviewer_whiteboard = whiteboard
413+ else:
414+ naked_product.reviewer_whiteboard += '\n' + whiteboard
415
416=== modified file 'lib/lp/registry/tests/test_product.py'
417--- lib/lp/registry/tests/test_product.py 2012-03-15 14:59:35 +0000
418+++ lib/lp/registry/tests/test_product.py 2012-03-15 14:59:36 +0000
419@@ -11,6 +11,7 @@
420 from testtools.matchers import MatchesAll
421 import transaction
422 from zope.component import getUtility
423+from zope.lifecycleevent.interfaces import IObjectModifiedEvent
424 from zope.security.interfaces import Unauthorized
425 from zope.security.proxy import removeSecurityProxy
426
427@@ -57,6 +58,7 @@
428 TestCaseWithFactory,
429 WebServiceTestCase,
430 )
431+from lp.testing.event import TestEventListener
432 from lp.testing.layers import (
433 DatabaseFunctionalLayer,
434 LaunchpadFunctionalLayer,
435@@ -461,6 +463,19 @@
436 """Test the rules of licenses and commercial subscriptions."""
437
438 layer = DatabaseFunctionalLayer
439+ event_listener = None
440+
441+ def setup_event_listener(self):
442+ self.events = []
443+ if self.event_listener is None:
444+ self.event_listener = TestEventListener(
445+ IProduct, IObjectModifiedEvent, self.on_event)
446+ else:
447+ self.event_listener._active = True
448+ self.addCleanup(self.event_listener.unregister)
449+
450+ def on_event(self, thing, event):
451+ self.events.append(event)
452
453 def test_getLicenses(self):
454 # License are assigned a list, but return a tuple.
455@@ -474,10 +489,24 @@
456 product = self.factory.makeProduct(licenses=[License.MIT])
457 with celebrity_logged_in('registry_experts'):
458 product.project_reviewed = True
459+ self.setup_event_listener()
460 with person_logged_in(product.owner):
461 product.licenses = [License.MIT]
462 with celebrity_logged_in('registry_experts'):
463 self.assertIs(True, product.project_reviewed)
464+ self.assertEqual([], self.events)
465+
466+ def test_setLicense(self):
467+ # The project_reviewed property is not reset, if the new licenses
468+ # are identical to the current licenses.
469+ product = self.factory.makeProduct()
470+ self.setup_event_listener()
471+ with person_logged_in(product.owner):
472+ product.licenses = [License.MIT]
473+ self.assertEqual((License.MIT, ), product.licenses)
474+ self.assertEqual(1, len(self.events))
475+ self.assertEqual(product, self.events[0].object)
476+ self.assertEqual(['licenses'], self.events[0].edited_fields)
477
478 def test_setLicense_also_sets_reviewed(self):
479 # The project_reviewed attribute it set to False if the licenses
480@@ -563,9 +592,10 @@
481 # New proprietary projects are given a complimentary 30 day
482 # commercial subscription.
483 owner = self.factory.makePerson()
484- product = getUtility(IProductSet).createProduct(
485- owner, 'fnord', 'Fnord', 'Fnord', 'test 1', 'test 2',
486- licenses=[License.OTHER_PROPRIETARY])
487+ with person_logged_in(owner):
488+ product = getUtility(IProductSet).createProduct(
489+ owner, 'fnord', 'Fnord', 'Fnord', 'test 1', 'test 2',
490+ licenses=[License.OTHER_PROPRIETARY])
491 with celebrity_logged_in('admin'):
492 cs = product.commercial_subscription
493 self.assertIsNotNone(cs)
494
495=== added file 'lib/lp/registry/tests/test_subscribers.py'
496--- lib/lp/registry/tests/test_subscribers.py 1970-01-01 00:00:00 +0000
497+++ lib/lp/registry/tests/test_subscribers.py 2012-03-15 14:59:36 +0000
498@@ -0,0 +1,152 @@
499+# Copyright 2012 Canonical Ltd. This software is licensed under the
500+# GNU Affero General Public License version 3 (see the file LICENSE).
501+
502+"""Test subscruber classes and functions."""
503+
504+__metaclass__ = type
505+
506+from datetime import datetime
507+
508+import pytz
509+
510+from zope.security.proxy import removeSecurityProxy
511+from lazr.lifecycle.event import ObjectModifiedEvent
512+
513+from lp.registry.interfaces.product import License
514+from lp.registry.subscribers import (
515+ LicenseNotification,
516+ product_licenses_modified,
517+ )
518+from lp.testing import (
519+ login_person,
520+ TestCaseWithFactory,
521+ )
522+from lp.testing.layers import DatabaseFunctionalLayer
523+from lp.testing.mail_helpers import pop_notifications
524+
525+
526+class ProductLicensesModifiedTestCase(TestCaseWithFactory):
527+
528+ layer = DatabaseFunctionalLayer
529+
530+ def make_product_event(self, licenses, edited_fields='licenses'):
531+ product = self.factory.makeProduct(licenses=licenses)
532+ pop_notifications()
533+ login_person(product.owner)
534+ event = ObjectModifiedEvent(
535+ product, product, edited_fields, user=product.owner)
536+ return product, event
537+
538+ def test_product_licenses_modified_licenses_not_edited(self):
539+ product, event = self.make_product_event(
540+ [License.OTHER_PROPRIETARY], edited_fields='_owner')
541+ product_licenses_modified(product, event)
542+ notifications = pop_notifications()
543+ self.assertEqual(0, len(notifications))
544+
545+ def test_product_licenses_modified_licenses_common_license(self):
546+ product, event = self.make_product_event([License.MIT])
547+ product_licenses_modified(product, event)
548+ notifications = pop_notifications()
549+ self.assertEqual(0, len(notifications))
550+
551+ def test_product_licenses_modified_licenses_other_proprietary(self):
552+ product, event = self.make_product_event([License.OTHER_PROPRIETARY])
553+ product_licenses_modified(product, event)
554+ notifications = pop_notifications()
555+ self.assertEqual(1, len(notifications))
556+
557+ def test_product_licenses_modified_licenses_other_open_source(self):
558+ product, event = self.make_product_event([License.OTHER_OPEN_SOURCE])
559+ product_licenses_modified(product, event)
560+ notifications = pop_notifications()
561+ self.assertEqual(1, len(notifications))
562+
563+ def test_product_licenses_modified_licenses_other_dont_know(self):
564+ product, event = self.make_product_event([License.DONT_KNOW])
565+ product_licenses_modified(product, event)
566+ notifications = pop_notifications()
567+ self.assertEqual(1, len(notifications))
568+
569+
570+class LicenseNotificationTestCase(TestCaseWithFactory):
571+
572+ layer = DatabaseFunctionalLayer
573+
574+ def make_product_user(self, licenses):
575+ # Setup an a view that implements ProductLicenseMixin.
576+ super(LicenseNotificationTestCase, self).setUp()
577+ user = self.factory.makePerson(
578+ name='registrant', email='registrant@launchpad.dev')
579+ login_person(user)
580+ product = self.factory.makeProduct(
581+ name='ball', owner=user, licenses=licenses)
582+ pop_notifications()
583+ return product, user
584+
585+ def verify_whiteboard(self, product):
586+ # Verify that the review whiteboard was updated.
587+ naked_product = removeSecurityProxy(product)
588+ entries = naked_product.reviewer_whiteboard.split('\n')
589+ whiteboard, stamp = entries[-1].rsplit(' ', 1)
590+ self.assertEqual(
591+ 'User notified of license policy on', whiteboard)
592+
593+ def verify_user_email(self, notification):
594+ # Verify that the user was sent an email about the license change.
595+ self.assertEqual(
596+ 'License information for ball in Launchpad',
597+ notification['Subject'])
598+ self.assertEqual(
599+ 'Registrant <registrant@launchpad.dev>',
600+ notification['To'])
601+ self.assertEqual(
602+ 'Commercial <commercial@launchpad.net>',
603+ notification['Reply-To'])
604+
605+ def test_send_known_license(self):
606+ # A known license does not generate an email.
607+ product, user = self.make_product_user([License.GNU_GPL_V2])
608+ notification = LicenseNotification(product, user)
609+ result = notification.send()
610+ self.assertIs(False, result)
611+ self.assertEqual(0, len(pop_notifications()))
612+
613+ def test_send_other_dont_know(self):
614+ # An Other/I don't know license sends one email.
615+ product, user = self.make_product_user([License.DONT_KNOW])
616+ notification = LicenseNotification(product, user)
617+ result = notification.send()
618+ self.assertIs(True, result)
619+ self.verify_whiteboard(product)
620+ notifications = pop_notifications()
621+ self.assertEqual(1, len(notifications))
622+ self.verify_user_email(notifications.pop())
623+
624+ def test_send_other_open_source(self):
625+ # An Other/Open Source license sends one email.
626+ product, user = self.make_product_user([License.OTHER_OPEN_SOURCE])
627+ notification = LicenseNotification(product, user)
628+ result = notification.send()
629+ self.assertIs(True, result)
630+ self.verify_whiteboard(product)
631+ notifications = pop_notifications()
632+ self.assertEqual(1, len(notifications))
633+ self.verify_user_email(notifications.pop())
634+
635+ def test_send_other_proprietary(self):
636+ # An Other/Proprietary license sends one email.
637+ product, user = self.make_product_user([License.OTHER_PROPRIETARY])
638+ notification = LicenseNotification(product, user)
639+ result = notification.send()
640+ self.assertIs(True, result)
641+ self.verify_whiteboard(product)
642+ notifications = pop_notifications()
643+ self.assertEqual(1, len(notifications))
644+ self.verify_user_email(notifications.pop())
645+
646+ def test_formatDate(self):
647+ # Verify the date format.
648+ now = datetime(2005, 6, 15, 0, 0, 0, 0, pytz.UTC)
649+ result = LicenseNotification._formatDate(now)
650+ self.assertEqual('2005-06-15', result)
651
652=== modified file 'lib/lp/testing/factory.py'
653--- lib/lp/testing/factory.py 2012-03-14 01:13:46 +0000
654+++ lib/lp/testing/factory.py 2012-03-15 14:59:36 +0000
655@@ -967,17 +967,19 @@
656 title = self.getUniqueString('title')
657 if summary is None:
658 summary = self.getUniqueString('summary')
659- product = getUtility(IProductSet).createProduct(
660- owner,
661- name,
662- displayname,
663- title,
664- summary,
665- self.getUniqueString('description'),
666- licenses=licenses,
667- project=project,
668- registrant=registrant,
669- icon=icon)
670+ admins = getUtility(ILaunchpadCelebrities).admin
671+ with person_logged_in(admins.teamowner):
672+ product = getUtility(IProductSet).createProduct(
673+ owner,
674+ name,
675+ displayname,
676+ title,
677+ summary,
678+ self.getUniqueString('description'),
679+ licenses=licenses,
680+ project=project,
681+ registrant=registrant,
682+ icon=icon)
683 naked_product = removeSecurityProxy(product)
684 if official_malone is not None:
685 naked_product.official_malone = official_malone