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
=== modified file 'lib/lp/registry/browser/product.py'
--- lib/lp/registry/browser/product.py 2012-03-08 17:08:56 +0000
+++ lib/lp/registry/browser/product.py 2012-03-15 14:59:36 +0000
@@ -77,7 +77,6 @@
77 SimpleTerm,77 SimpleTerm,
78 SimpleVocabulary,78 SimpleVocabulary,
79 )79 )
80from zope.security.proxy import removeSecurityProxy
8180
82from lp import _81from lp import _
83from lp.answers.browser.faqtarget import FAQTargetNavigationMixin82from lp.answers.browser.faqtarget import FAQTargetNavigationMixin
@@ -177,11 +176,6 @@
177 PublicPersonChoice,176 PublicPersonChoice,
178 )177 )
179from lp.services.librarian.interfaces import ILibraryFileAliasSet178from lp.services.librarian.interfaces import ILibraryFileAliasSet
180from lp.services.mail.helpers import get_email_template
181from lp.services.mail.sendmail import (
182 format_address,
183 simple_sendmail,
184 )
185from lp.services.propertycache import cachedproperty179from lp.services.propertycache import cachedproperty
186from lp.services.webapp import (180from lp.services.webapp import (
187 ApplicationMenu,181 ApplicationMenu,
@@ -200,7 +194,6 @@
200from lp.services.webapp.batching import BatchNavigator194from lp.services.webapp.batching import BatchNavigator
201from lp.services.webapp.breadcrumb import Breadcrumb195from lp.services.webapp.breadcrumb import Breadcrumb
202from lp.services.webapp.interfaces import (196from lp.services.webapp.interfaces import (
203 ILaunchBag,
204 UnsafeFormGetSubmissionError,197 UnsafeFormGetSubmissionError,
205 )198 )
206from lp.services.webapp.menu import NavigationMenu199from lp.services.webapp.menu import NavigationMenu
@@ -309,74 +302,6 @@
309 # Launchpad is ok with all licenses used in this project.302 # Launchpad is ok with all licenses used in this project.
310 pass303 pass
311304
312 def notifyCommercialMailingList(self):
313 """Notify user about Launchpad license rules."""
314 licenses = list(self.product.licenses)
315 needs_email = (
316 License.OTHER_PROPRIETARY in licenses
317 or License.OTHER_OPEN_SOURCE in licenses
318 or [License.DONT_KNOW] == licenses)
319 if not needs_email:
320 # The project has a recognized license.
321 return
322
323 def indent(text):
324 if text is None:
325 return None
326 text = '\n '.join(line for line in text.split('\n'))
327 text = ' ' + text
328 return text
329
330 user = getUtility(ILaunchBag).user
331 user_address = format_address(
332 user.displayname, user.preferredemail.email)
333 from_address = format_address(
334 "Launchpad", config.canonical.noreply_from_address)
335 commercial_address = format_address(
336 'Commercial', 'commercial@launchpad.net')
337 license_titles = '\n'.join(
338 license.title for license in self.product.licenses)
339 substitutions = dict(
340 user_browsername=user.displayname,
341 user_name=user.name,
342 product_name=self.product.name,
343 product_url=canonical_url(self.product),
344 product_summary=indent(self.product.summary),
345 license_titles=indent(license_titles),
346 license_info=indent(self.product.license_info))
347 # Email the user about license policy.
348 subject = (
349 "License information for %(product_name)s "
350 "in Launchpad" % substitutions)
351 template = get_email_template(
352 'product-other-license.txt', app='registry')
353 message = template % substitutions
354 simple_sendmail(
355 from_address, user_address,
356 subject, message, headers={'Reply-To': commercial_address})
357 # Inform that Launchpad recognized the license change.
358 self._addLicenseChangeToReviewWhiteboard()
359 self.request.response.addInfoNotification(_(
360 "Launchpad is free to use for software under approved "
361 "licenses. The Launchpad team will be in contact with "
362 "you soon."))
363
364 def _addLicenseChangeToReviewWhiteboard(self):
365 """Update the whiteboard for the reviewer's benefit."""
366 now = self._formatDate()
367 whiteboard = 'User notified of license policy on %s.' % now
368 naked_product = removeSecurityProxy(self.product)
369 if naked_product.reviewer_whiteboard is None:
370 naked_product.reviewer_whiteboard = whiteboard
371 else:
372 naked_product.reviewer_whiteboard += '\n' + whiteboard
373
374 def _formatDate(self, now=None):
375 """Return the date formatted for messages."""
376 if now is None:
377 now = datetime.now(tz=pytz.UTC)
378 return now.strftime('%Y-%m-%d')
379
380305
381class ProductFacets(QuestionTargetFacetMixin, StandardLaunchpadFacets):306class ProductFacets(QuestionTargetFacetMixin, StandardLaunchpadFacets):
382 """The links that will appear in the facet menu for an IProduct."""307 """The links that will appear in the facet menu for an IProduct."""
@@ -1620,13 +1545,7 @@
16201545
1621 @action("Change", name='change')1546 @action("Change", name='change')
1622 def change_action(self, action, data):1547 def change_action(self, action, data):
1623 previous_licenses = self.context.licenses
1624 self.updateContextFromData(data)1548 self.updateContextFromData(data)
1625 # only send email the first time licenses are set
1626 if len(previous_licenses) == 0:
1627 # self.product is expected by notifyCommercialMailingList
1628 self.product = self.context
1629 self.notifyCommercialMailingList()
16301549
1631 @property1550 @property
1632 def next_url(self):1551 def next_url(self):
@@ -2284,7 +2203,6 @@
2284 def main_action(self, data):2203 def main_action(self, data):
2285 """See `MultiStepView`."""2204 """See `MultiStepView`."""
2286 self.product = self.create_product(data)2205 self.product = self.create_product(data)
2287 self.notifyCommercialMailingList()
2288 notify(ObjectCreatedEvent(self.product))2206 notify(ObjectCreatedEvent(self.product))
2289 self.link_source_package(self.product, data)2207 self.link_source_package(self.product, data)
22902208
22912209
=== modified file 'lib/lp/registry/browser/tests/test_product.py'
--- lib/lp/registry/browser/tests/test_product.py 2012-03-02 06:46:29 +0000
+++ lib/lp/registry/browser/tests/test_product.py 2012-03-15 14:59:36 +0000
@@ -5,14 +5,9 @@
55
6__metaclass__ = type6__metaclass__ = type
77
8import datetime
9
10import pytz
11from zope.component import getUtility8from zope.component import getUtility
12from zope.security.proxy import removeSecurityProxy
139
14from lp.app.enums import ServiceUsage10from lp.app.enums import ServiceUsage
15from lp.registry.browser.product import ProductLicenseMixin
16from lp.registry.interfaces.product import (11from lp.registry.interfaces.product import (
17 IProductSet,12 IProductSet,
18 License,13 License,
@@ -22,13 +17,11 @@
22from lp.testing import (17from lp.testing import (
23 BrowserTestCase,18 BrowserTestCase,
24 login_celebrity,19 login_celebrity,
25 login_person,
26 person_logged_in,20 person_logged_in,
27 TestCaseWithFactory,21 TestCaseWithFactory,
28 )22 )
29from lp.testing.fixture import DemoMode23from lp.testing.fixture import DemoMode
30from lp.testing.layers import DatabaseFunctionalLayer24from lp.testing.layers import DatabaseFunctionalLayer
31from lp.testing.mail_helpers import pop_notifications
32from lp.testing.pages import find_tag_by_id25from lp.testing.pages import find_tag_by_id
33from lp.testing.service_usage_helpers import set_service_usage26from lp.testing.service_usage_helpers import set_service_usage
34from lp.testing.views import (27from lp.testing.views import (
@@ -37,86 +30,6 @@
37 )30 )
3831
3932
40class TestProductLicenseMixin(TestCaseWithFactory):
41
42 layer = DatabaseFunctionalLayer
43
44 def setUp(self):
45 # Setup an a view that implements ProductLicenseMixin.
46 super(TestProductLicenseMixin, self).setUp()
47 self.registrant = self.factory.makePerson(
48 name='registrant', email='registrant@launchpad.dev')
49 self.product = self.factory.makeProduct(
50 name='ball', owner=self.registrant)
51 self.view = create_view(self.product, '+edit')
52 self.view.product = self.product
53 login_person(self.registrant)
54
55 def verify_whiteboard(self):
56 # Verify that the review whiteboard was updated.
57 naked_product = removeSecurityProxy(self.product)
58 whiteboard, stamp = naked_product.reviewer_whiteboard.rsplit(' ', 1)
59 self.assertEqual(
60 'User notified of license policy on', whiteboard)
61
62 def verify_user_email(self, notification):
63 # Verify that the user was sent an email about the license change.
64 self.assertEqual(
65 'License information for ball in Launchpad',
66 notification['Subject'])
67 self.assertEqual(
68 'Registrant <registrant@launchpad.dev>',
69 notification['To'])
70 self.assertEqual(
71 'Commercial <commercial@launchpad.net>',
72 notification['Reply-To'])
73
74 def test_ProductLicenseMixin_instance(self):
75 # The object under test is an instance of ProductLicenseMixin.
76 self.assertTrue(isinstance(self.view, ProductLicenseMixin))
77
78 def test_notifyCommercialMailingList_known_license(self):
79 # A known license does not generate an email.
80 self.product.licenses = [License.GNU_GPL_V2]
81 self.view.notifyCommercialMailingList()
82 self.assertEqual(0, len(pop_notifications()))
83
84 def test_notifyCommercialMailingList_other_dont_know(self):
85 # An Other/I don't know license sends one email.
86 self.product.licenses = [License.DONT_KNOW]
87 self.view.notifyCommercialMailingList()
88 self.verify_whiteboard()
89 notifications = pop_notifications()
90 self.assertEqual(1, len(notifications))
91 self.verify_user_email(notifications.pop())
92
93 def test_notifyCommercialMailingList_other_open_source(self):
94 # An Other/Open Source license sends one email.
95 self.product.licenses = [License.OTHER_OPEN_SOURCE]
96 self.product.license_info = 'http://www,boost.org/'
97 self.view.notifyCommercialMailingList()
98 self.verify_whiteboard()
99 notifications = pop_notifications()
100 self.assertEqual(1, len(notifications))
101 self.verify_user_email(notifications.pop())
102
103 def test_notifyCommercialMailingList_other_proprietary(self):
104 # An Other/Proprietary license sends one email.
105 self.product.licenses = [License.OTHER_PROPRIETARY]
106 self.product.license_info = 'All mine'
107 self.view.notifyCommercialMailingList()
108 self.verify_whiteboard()
109 notifications = pop_notifications()
110 self.assertEqual(1, len(notifications))
111 self.verify_user_email(notifications.pop())
112
113 def test__formatDate(self):
114 # Verify the date format.
115 now = datetime.datetime(2005, 6, 15, 0, 0, 0, 0, pytz.UTC)
116 result = self.view._formatDate(now)
117 self.assertEqual('2005-06-15', result)
118
119
120class TestProductConfiguration(TestCaseWithFactory):33class TestProductConfiguration(TestCaseWithFactory):
121 """Tests the configuration links and helpers."""34 """Tests the configuration links and helpers."""
12235
12336
=== modified file 'lib/lp/registry/configure.zcml'
--- lib/lp/registry/configure.zcml 2012-02-24 07:17:43 +0000
+++ lib/lp/registry/configure.zcml 2012-03-15 14:59:36 +0000
@@ -1056,7 +1056,7 @@
1056 </class>1056 </class>
1057 <subscriber1057 <subscriber
1058 for="lp.registry.interfaces.product.IProduct zope.lifecycleevent.interfaces.IObjectModifiedEvent"1058 for="lp.registry.interfaces.product.IProduct zope.lifecycleevent.interfaces.IObjectModifiedEvent"
1059 handler="lp.registry.subscribers.product_modified"/>1059 handler="lp.registry.subscribers.product_licenses_modified"/>
1060 <class1060 <class
1061 class="lp.registry.model.mailinglist.MailingList">1061 class="lp.registry.model.mailinglist.MailingList">
1062 <allow1062 <allow
10631063
=== modified file 'lib/lp/registry/model/product.py'
--- lib/lp/registry/model/product.py 2012-03-15 14:59:35 +0000
+++ lib/lp/registry/model/product.py 2012-03-15 14:59:36 +0000
@@ -788,6 +788,8 @@
788 registrant=lp_janitor, purchaser=lp_janitor,788 registrant=lp_janitor, purchaser=lp_janitor,
789 sales_system_id=sales_system_id, whiteboard=whiteboard)789 sales_system_id=sales_system_id, whiteboard=whiteboard)
790 get_property_cache(self).commercial_subscription = subscription790 get_property_cache(self).commercial_subscription = subscription
791 # Do not use a snapshot because the past is unintersting.
792 notify(ObjectModifiedEvent(self, self, edited_fields=['licenses']))
791793
792 licenses = property(_getLicenses, _setLicenses)794 licenses = property(_getLicenses, _setLicenses)
793795
794796
=== modified file 'lib/lp/registry/stories/product/xx-product-index.txt'
--- lib/lp/registry/stories/product/xx-product-index.txt 2011-12-30 06:14:56 +0000
+++ lib/lp/registry/stories/product/xx-product-index.txt 2012-03-15 14:59:36 +0000
@@ -192,9 +192,11 @@
192much time left on its commercial subscription, a portlet is displayed to192much time left on its commercial subscription, a portlet is displayed to
193direct the owner to purchase a subscription.193direct the owner to purchase a subscription.
194194
195 >>> login_person(firefox.owner)
195 >>> firefox.licenses = [License.OTHER_PROPRIETARY]196 >>> firefox.licenses = [License.OTHER_PROPRIETARY]
196 >>> flush_database_updates()197 >>> flush_database_updates()
197 >>> transaction.commit()198 >>> transaction.commit()
199 >>> logout()
198 >>> owner_browser.open('http://launchpad.dev/firefox')200 >>> owner_browser.open('http://launchpad.dev/firefox')
199 >>> print find_tag_by_id(owner_browser.contents, 'license-status')201 >>> print find_tag_by_id(owner_browser.contents, 'license-status')
200 <...This project&rsquo;s license is proprietary...202 <...This project&rsquo;s license is proprietary...
201203
=== modified file 'lib/lp/registry/subscribers.py'
--- lib/lp/registry/subscribers.py 2009-06-25 04:06:00 +0000
+++ lib/lp/registry/subscribers.py 2012-03-15 14:59:36 +0000
@@ -1,13 +1,114 @@
1# Copyright 2009 Canonical Ltd. This software is licensed under the1# Copyright 2009 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
3"""Functions and classes that are subscribed to registry events."""
34
4__metaclass__ = type5__metaclass__ = type
56
6__all__ = [7__all__ = [
7 'product_modified',8 'product_licenses_modified',
8 ]9 ]
910
1011from datetime import datetime
11def product_modified(product, event):12
12 pass13import pytz
1314
15from zope.security.proxy import removeSecurityProxy
16
17from lp.registry.interfaces.person import IPerson
18from lp.registry.interfaces.product import License
19from lp.services.config import config
20from lp.services.mail.helpers import get_email_template
21from lp.services.mail.sendmail import (
22 format_address,
23 simple_sendmail,
24 )
25from lp.services.webapp.publisher import canonical_url
26
27
28def product_licenses_modified(product, event):
29 """Send a notification if licenses changed and a license is special."""
30 if not event.edited_fields:
31 return
32 licenses_changed = 'licenses' in event.edited_fields
33 needs_notification = LicenseNotification.needs_notification(product)
34 if licenses_changed and needs_notification:
35 user = IPerson(event.user)
36 notification = LicenseNotification(product, user)
37 notification.send()
38
39
40class LicenseNotification:
41 """Send notification about special licenses to the user."""
42
43 def __init__(self, product, user):
44 self.product = product
45 self.user = user
46
47 @staticmethod
48 def needs_notification(product):
49 licenses = list(product.licenses)
50 return (
51 License.OTHER_PROPRIETARY in licenses
52 or License.OTHER_OPEN_SOURCE in licenses
53 or [License.DONT_KNOW] == licenses)
54
55 def send(self):
56 """Send a message to the user about the product's license."""
57 if not self.needs_notification(self.product):
58 # The project has a common license.
59 return False
60 user_address = format_address(
61 self.user.displayname, self.user.preferredemail.email)
62 from_address = format_address(
63 "Launchpad", config.canonical.noreply_from_address)
64 commercial_address = format_address(
65 'Commercial', 'commercial@launchpad.net')
66 license_titles = '\n'.join(
67 license.title for license in self.product.licenses)
68 substitutions = dict(
69 user_browsername=self.user.displayname,
70 user_name=self.user.name,
71 product_name=self.product.name,
72 product_url=canonical_url(self.product),
73 product_summary=self._indent(self.product.summary),
74 license_titles=self._indent(license_titles),
75 license_info=self._indent(self.product.license_info))
76 # Email the user about license policy.
77 subject = (
78 "License information for %(product_name)s "
79 "in Launchpad" % substitutions)
80 template = get_email_template(
81 'product-other-license.txt', app='registry')
82 message = template % substitutions
83 simple_sendmail(
84 from_address, user_address,
85 subject, message, headers={'Reply-To': commercial_address})
86 # Inform that Launchpad recognized the license change.
87 self._addLicenseChangeToReviewWhiteboard()
88 return True
89
90 @staticmethod
91 def _indent(text):
92 """Indent the text to be included in the message."""
93 if text is None:
94 return None
95 text = '\n '.join(line for line in text.split('\n'))
96 text = ' ' + text
97 return text
98
99 @staticmethod
100 def _formatDate(now=None):
101 """Return the date formatted for messages."""
102 if now is None:
103 now = datetime.now(tz=pytz.UTC)
104 return now.strftime('%Y-%m-%d')
105
106 def _addLicenseChangeToReviewWhiteboard(self):
107 """Update the whiteboard for the reviewer's benefit."""
108 now = self._formatDate()
109 whiteboard = 'User notified of license policy on %s.' % now
110 naked_product = removeSecurityProxy(self.product)
111 if naked_product.reviewer_whiteboard is None:
112 naked_product.reviewer_whiteboard = whiteboard
113 else:
114 naked_product.reviewer_whiteboard += '\n' + whiteboard
14115
=== modified file 'lib/lp/registry/tests/test_product.py'
--- lib/lp/registry/tests/test_product.py 2012-03-15 14:59:35 +0000
+++ lib/lp/registry/tests/test_product.py 2012-03-15 14:59:36 +0000
@@ -11,6 +11,7 @@
11from testtools.matchers import MatchesAll11from testtools.matchers import MatchesAll
12import transaction12import transaction
13from zope.component import getUtility13from zope.component import getUtility
14from zope.lifecycleevent.interfaces import IObjectModifiedEvent
14from zope.security.interfaces import Unauthorized15from zope.security.interfaces import Unauthorized
15from zope.security.proxy import removeSecurityProxy16from zope.security.proxy import removeSecurityProxy
1617
@@ -57,6 +58,7 @@
57 TestCaseWithFactory,58 TestCaseWithFactory,
58 WebServiceTestCase,59 WebServiceTestCase,
59 )60 )
61from lp.testing.event import TestEventListener
60from lp.testing.layers import (62from lp.testing.layers import (
61 DatabaseFunctionalLayer,63 DatabaseFunctionalLayer,
62 LaunchpadFunctionalLayer,64 LaunchpadFunctionalLayer,
@@ -461,6 +463,19 @@
461 """Test the rules of licenses and commercial subscriptions."""463 """Test the rules of licenses and commercial subscriptions."""
462464
463 layer = DatabaseFunctionalLayer465 layer = DatabaseFunctionalLayer
466 event_listener = None
467
468 def setup_event_listener(self):
469 self.events = []
470 if self.event_listener is None:
471 self.event_listener = TestEventListener(
472 IProduct, IObjectModifiedEvent, self.on_event)
473 else:
474 self.event_listener._active = True
475 self.addCleanup(self.event_listener.unregister)
476
477 def on_event(self, thing, event):
478 self.events.append(event)
464479
465 def test_getLicenses(self):480 def test_getLicenses(self):
466 # License are assigned a list, but return a tuple.481 # License are assigned a list, but return a tuple.
@@ -474,10 +489,24 @@
474 product = self.factory.makeProduct(licenses=[License.MIT])489 product = self.factory.makeProduct(licenses=[License.MIT])
475 with celebrity_logged_in('registry_experts'):490 with celebrity_logged_in('registry_experts'):
476 product.project_reviewed = True491 product.project_reviewed = True
492 self.setup_event_listener()
477 with person_logged_in(product.owner):493 with person_logged_in(product.owner):
478 product.licenses = [License.MIT]494 product.licenses = [License.MIT]
479 with celebrity_logged_in('registry_experts'):495 with celebrity_logged_in('registry_experts'):
480 self.assertIs(True, product.project_reviewed)496 self.assertIs(True, product.project_reviewed)
497 self.assertEqual([], self.events)
498
499 def test_setLicense(self):
500 # The project_reviewed property is not reset, if the new licenses
501 # are identical to the current licenses.
502 product = self.factory.makeProduct()
503 self.setup_event_listener()
504 with person_logged_in(product.owner):
505 product.licenses = [License.MIT]
506 self.assertEqual((License.MIT, ), product.licenses)
507 self.assertEqual(1, len(self.events))
508 self.assertEqual(product, self.events[0].object)
509 self.assertEqual(['licenses'], self.events[0].edited_fields)
481510
482 def test_setLicense_also_sets_reviewed(self):511 def test_setLicense_also_sets_reviewed(self):
483 # The project_reviewed attribute it set to False if the licenses512 # The project_reviewed attribute it set to False if the licenses
@@ -563,9 +592,10 @@
563 # New proprietary projects are given a complimentary 30 day592 # New proprietary projects are given a complimentary 30 day
564 # commercial subscription.593 # commercial subscription.
565 owner = self.factory.makePerson()594 owner = self.factory.makePerson()
566 product = getUtility(IProductSet).createProduct(595 with person_logged_in(owner):
567 owner, 'fnord', 'Fnord', 'Fnord', 'test 1', 'test 2',596 product = getUtility(IProductSet).createProduct(
568 licenses=[License.OTHER_PROPRIETARY])597 owner, 'fnord', 'Fnord', 'Fnord', 'test 1', 'test 2',
598 licenses=[License.OTHER_PROPRIETARY])
569 with celebrity_logged_in('admin'):599 with celebrity_logged_in('admin'):
570 cs = product.commercial_subscription600 cs = product.commercial_subscription
571 self.assertIsNotNone(cs)601 self.assertIsNotNone(cs)
572602
=== added file 'lib/lp/registry/tests/test_subscribers.py'
--- lib/lp/registry/tests/test_subscribers.py 1970-01-01 00:00:00 +0000
+++ lib/lp/registry/tests/test_subscribers.py 2012-03-15 14:59:36 +0000
@@ -0,0 +1,152 @@
1# Copyright 2012 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Test subscruber classes and functions."""
5
6__metaclass__ = type
7
8from datetime import datetime
9
10import pytz
11
12from zope.security.proxy import removeSecurityProxy
13from lazr.lifecycle.event import ObjectModifiedEvent
14
15from lp.registry.interfaces.product import License
16from lp.registry.subscribers import (
17 LicenseNotification,
18 product_licenses_modified,
19 )
20from lp.testing import (
21 login_person,
22 TestCaseWithFactory,
23 )
24from lp.testing.layers import DatabaseFunctionalLayer
25from lp.testing.mail_helpers import pop_notifications
26
27
28class ProductLicensesModifiedTestCase(TestCaseWithFactory):
29
30 layer = DatabaseFunctionalLayer
31
32 def make_product_event(self, licenses, edited_fields='licenses'):
33 product = self.factory.makeProduct(licenses=licenses)
34 pop_notifications()
35 login_person(product.owner)
36 event = ObjectModifiedEvent(
37 product, product, edited_fields, user=product.owner)
38 return product, event
39
40 def test_product_licenses_modified_licenses_not_edited(self):
41 product, event = self.make_product_event(
42 [License.OTHER_PROPRIETARY], edited_fields='_owner')
43 product_licenses_modified(product, event)
44 notifications = pop_notifications()
45 self.assertEqual(0, len(notifications))
46
47 def test_product_licenses_modified_licenses_common_license(self):
48 product, event = self.make_product_event([License.MIT])
49 product_licenses_modified(product, event)
50 notifications = pop_notifications()
51 self.assertEqual(0, len(notifications))
52
53 def test_product_licenses_modified_licenses_other_proprietary(self):
54 product, event = self.make_product_event([License.OTHER_PROPRIETARY])
55 product_licenses_modified(product, event)
56 notifications = pop_notifications()
57 self.assertEqual(1, len(notifications))
58
59 def test_product_licenses_modified_licenses_other_open_source(self):
60 product, event = self.make_product_event([License.OTHER_OPEN_SOURCE])
61 product_licenses_modified(product, event)
62 notifications = pop_notifications()
63 self.assertEqual(1, len(notifications))
64
65 def test_product_licenses_modified_licenses_other_dont_know(self):
66 product, event = self.make_product_event([License.DONT_KNOW])
67 product_licenses_modified(product, event)
68 notifications = pop_notifications()
69 self.assertEqual(1, len(notifications))
70
71
72class LicenseNotificationTestCase(TestCaseWithFactory):
73
74 layer = DatabaseFunctionalLayer
75
76 def make_product_user(self, licenses):
77 # Setup an a view that implements ProductLicenseMixin.
78 super(LicenseNotificationTestCase, self).setUp()
79 user = self.factory.makePerson(
80 name='registrant', email='registrant@launchpad.dev')
81 login_person(user)
82 product = self.factory.makeProduct(
83 name='ball', owner=user, licenses=licenses)
84 pop_notifications()
85 return product, user
86
87 def verify_whiteboard(self, product):
88 # Verify that the review whiteboard was updated.
89 naked_product = removeSecurityProxy(product)
90 entries = naked_product.reviewer_whiteboard.split('\n')
91 whiteboard, stamp = entries[-1].rsplit(' ', 1)
92 self.assertEqual(
93 'User notified of license policy on', whiteboard)
94
95 def verify_user_email(self, notification):
96 # Verify that the user was sent an email about the license change.
97 self.assertEqual(
98 'License information for ball in Launchpad',
99 notification['Subject'])
100 self.assertEqual(
101 'Registrant <registrant@launchpad.dev>',
102 notification['To'])
103 self.assertEqual(
104 'Commercial <commercial@launchpad.net>',
105 notification['Reply-To'])
106
107 def test_send_known_license(self):
108 # A known license does not generate an email.
109 product, user = self.make_product_user([License.GNU_GPL_V2])
110 notification = LicenseNotification(product, user)
111 result = notification.send()
112 self.assertIs(False, result)
113 self.assertEqual(0, len(pop_notifications()))
114
115 def test_send_other_dont_know(self):
116 # An Other/I don't know license sends one email.
117 product, user = self.make_product_user([License.DONT_KNOW])
118 notification = LicenseNotification(product, user)
119 result = notification.send()
120 self.assertIs(True, result)
121 self.verify_whiteboard(product)
122 notifications = pop_notifications()
123 self.assertEqual(1, len(notifications))
124 self.verify_user_email(notifications.pop())
125
126 def test_send_other_open_source(self):
127 # An Other/Open Source license sends one email.
128 product, user = self.make_product_user([License.OTHER_OPEN_SOURCE])
129 notification = LicenseNotification(product, user)
130 result = notification.send()
131 self.assertIs(True, result)
132 self.verify_whiteboard(product)
133 notifications = pop_notifications()
134 self.assertEqual(1, len(notifications))
135 self.verify_user_email(notifications.pop())
136
137 def test_send_other_proprietary(self):
138 # An Other/Proprietary license sends one email.
139 product, user = self.make_product_user([License.OTHER_PROPRIETARY])
140 notification = LicenseNotification(product, user)
141 result = notification.send()
142 self.assertIs(True, result)
143 self.verify_whiteboard(product)
144 notifications = pop_notifications()
145 self.assertEqual(1, len(notifications))
146 self.verify_user_email(notifications.pop())
147
148 def test_formatDate(self):
149 # Verify the date format.
150 now = datetime(2005, 6, 15, 0, 0, 0, 0, pytz.UTC)
151 result = LicenseNotification._formatDate(now)
152 self.assertEqual('2005-06-15', result)
0153
=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py 2012-03-14 01:13:46 +0000
+++ lib/lp/testing/factory.py 2012-03-15 14:59:36 +0000
@@ -967,17 +967,19 @@
967 title = self.getUniqueString('title')967 title = self.getUniqueString('title')
968 if summary is None:968 if summary is None:
969 summary = self.getUniqueString('summary')969 summary = self.getUniqueString('summary')
970 product = getUtility(IProductSet).createProduct(970 admins = getUtility(ILaunchpadCelebrities).admin
971 owner,971 with person_logged_in(admins.teamowner):
972 name,972 product = getUtility(IProductSet).createProduct(
973 displayname,973 owner,
974 title,974 name,
975 summary,975 displayname,
976 self.getUniqueString('description'),976 title,
977 licenses=licenses,977 summary,
978 project=project,978 self.getUniqueString('description'),
979 registrant=registrant,979 licenses=licenses,
980 icon=icon)980 project=project,
981 registrant=registrant,
982 icon=icon)
981 naked_product = removeSecurityProxy(product)983 naked_product = removeSecurityProxy(product)
982 if official_malone is not None:984 if official_malone is not None:
983 naked_product.official_malone = official_malone985 naked_product.official_malone = official_malone