← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~sinzui/launchpad/entitlement-3 into lp:launchpad

 

Curtis Hovey has proposed merging lp:~sinzui/launchpad/entitlement-3 into lp:launchpad with lp:~sinzui/launchpad/entitlement-2 as a prerequisite.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~sinzui/launchpad/entitlement-3/+merge/97300

Pre-implementation: StevenK

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 solves outstanding issues.

This branch replaces the generic email sent to the user about a special
licensing situation with one specific the the case.

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

RULES

    PREVIOUS BRANCHES
    * 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.
    * Move license email code from the view to the model so that API changes
      work.

    THIS 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 learn more
      more about commercial subscriptions and how to purchase them.
      * This email probably replaces the existing email that explain Lp's
        licensing and purchasing rules.

    FUTURE BRANCHES
    * 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 it does not have a commercial subscription
    * Use Change details to set the license to proprietary.
    * Verify it has a commercial subscription that expires in one month.
    * Verify a notice explains the situation and that privacy features
      are available.
    * Verify an email was sent to the maintainer explaining how to
      purchase a commercial subscription and what they provide.

    * Visit https://qastaging.launchpad.net/projects/+new and create a
      proprietary project.
    * Verify it has a commercial subscription that expires in one month.
    * Verify a notice explains the situation and how to configure proprietary
      features.
    * Verify an email was sent to the maintainer explaining how to
      purchase a commercial subscription and that privacy features are
      available.

    * Force the license to expire.
    * Set the open project project's license to proprietary
    * Verify an email explains that the commercial subscription has expired.


LINT

    lib/lp/registry/subscribers.py
    lib/lp/registry/emailtemplates/product-license-dont-know.txt
    lib/lp/registry/emailtemplates/product-license-other-open-source.txt
    lib/lp/registry/emailtemplates/product-license-other-proprietary.txt
    lib/lp/registry/tests/test_subscribers.py


TEST

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


IMPLEMENTATION

I split the generic licensing email into messages that explained the exact
situation. I think this will reduce the number of confused users who send
emails to feedback@xxxxxxxxxxxxx because the old email described situations
that were not pertinent. I updated the LicenseNotification object to create
a specific message about the state of the commercial license and include
it in the email and browser notices. The _indent() helper was unused, as
was some of the interpolated variables so I removed them.
    lib/lp/registry/subscribers.py
    lib/lp/registry/emailtemplates/product-license-dont-know.txt
    lib/lp/registry/emailtemplates/product-license-other-open-source.txt
    lib/lp/registry/emailtemplates/product-license-other-proprietary.txt
    lib/lp/registry/tests/test_subscribers.py
-- 
https://code.launchpad.net/~sinzui/launchpad/entitlement-3/+merge/97300
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~sinzui/launchpad/entitlement-3 into lp:launchpad.
=== added file 'lib/lp/registry/emailtemplates/product-license-dont-know.txt'
--- lib/lp/registry/emailtemplates/product-license-dont-know.txt	1970-01-01 00:00:00 +0000
+++ lib/lp/registry/emailtemplates/product-license-dont-know.txt	2012-03-13 21:09:22 +0000
@@ -0,0 +1,32 @@
+Hello %(user_displayname)s,
+
+You recently stated that you don't know the license of the project
+'%(product_name)s' in Launchpad.
+%(product_url)s
+
+We urge you to update the licensing in Launchpad as soon as you learn or
+decide the license so that other users understand the terms of that the
+projects is distributed under.  You can do so by following the 'Change
+Details' link on your project's overview page.
+
+Launchpad is a collaboration site that is free to use for projects
+with an approved open source license.  When you registered your
+project the list of licenses presented are the ones we automatically
+recognize.
+
+Other licenses must follow the guidelines we list on the following
+page in order to be approved:
+https://help.launchpad.net/Legal/ProjectLicensing
+
+Further information is on our FAQ "Can closed-source or proprietary
+projects use Launchpad?" which can be found at:
+https://answers.launchpad.net/launchpad/+faq/208
+
+If your project's licensing does not meet the guidelines we will
+consider it "Proprietary" and mark it as such in Launchpad.  Proprietary
+projects can use Launchpad by purchasing a commercial-use subscription
+which costs US$250/year/project.
+
+Thanks,
+
+The Launchpad team.

=== added file 'lib/lp/registry/emailtemplates/product-license-other-open-source.txt'
--- lib/lp/registry/emailtemplates/product-license-other-open-source.txt	1970-01-01 00:00:00 +0000
+++ lib/lp/registry/emailtemplates/product-license-other-open-source.txt	2012-03-13 21:09:22 +0000
@@ -0,0 +1,34 @@
+Hello %(user_displayname)s,
+
+You recently stated that the project '%(product_name)s'
+in Launchpad has an 'Other/Open Source' license.
+%(product_url)s
+
+Sometimes new projects are licensed as 'Other/Open Source' because the
+licensing decisions have not yet been made.  If that is your situation
+we urge you to update the licensing in Launchpad as soon as you make
+that choice.  If the license for your project needs to be corrected you
+can do so by following the 'Change Details' link on your project's
+overview page.
+
+Launchpad is a collaboration site that is free to use for projects
+with an approved open source license.  When you registered your
+project the list of licenses presented are the ones we automatically
+recognize.
+
+Other licenses must follow the guidelines we list on the following
+page in order to be approved:
+https://help.launchpad.net/Legal/ProjectLicensing
+
+Further information is on our FAQ "Can closed-source or proprietary
+projects use Launchpad?" which can be found at:
+https://answers.launchpad.net/launchpad/+faq/208
+
+If your project's licensing does not meet the guidelines we will
+consider it "Proprietary" and mark it as such in Launchpad.  Proprietary
+projects can use Launchpad by purchasing a commercial-use subscription
+which costs US$250/year/project.
+
+Thanks,
+
+The Launchpad team.

=== added file 'lib/lp/registry/emailtemplates/product-license-other-proprietary.txt'
--- lib/lp/registry/emailtemplates/product-license-other-proprietary.txt	1970-01-01 00:00:00 +0000
+++ lib/lp/registry/emailtemplates/product-license-other-proprietary.txt	2012-03-13 21:09:22 +0000
@@ -0,0 +1,39 @@
+Hello %(user_displayname)s,
+
+You recently stated that the project '%(product_name)s'
+in Launchpad is proprietary.
+%(product_url)s
+
+%(commercial_use_expiration)s
+
+Proprietary projects can use Launchpad by purchasing a commercial-use
+subscription which costs US$250/year/project.  Follow the instructions
+presented on your project overview page to purchase a subscription
+voucher.
+
+A commercial-use subscription allows you to host your commercial project
+on Launchpad in the same manner as any other project. Project's with a
+commercial-use subscription may have private-by-default bugs, and you
+may also request the setup of private code hosting. As the maintainer of
+a project with a commercial-use subscription, you may create private
+teams with private mailing lists and package archives.
+
+Launchpad is a collaboration site that is free to use for projects
+with an approved open source license.  When you registered your
+project the list of licenses presented are the ones we automatically
+recognize.
+
+Other licenses must follow the guidelines we list on the following
+page in order to be approved:
+https://help.launchpad.net/Legal/ProjectLicensing
+
+Further information is on our FAQ "Can closed-source or proprietary
+projects use Launchpad?" which can be found at:
+https://answers.launchpad.net/launchpad/+faq/208
+
+If the license for your project needs to be corrected you can do so by
+following the 'Change Details' link on your project's overview page.
+
+Thanks,
+
+The Launchpad team.

=== modified file 'lib/lp/registry/subscribers.py'
--- lib/lp/registry/subscribers.py	2012-03-13 21:09:21 +0000
+++ lib/lp/registry/subscribers.py	2012-03-13 21:09:22 +0000
@@ -9,6 +9,7 @@
     ]
 
 from datetime import datetime
+import textwrap
 
 import pytz
 
@@ -22,7 +23,11 @@
     format_address,
     simple_sendmail,
     )
-from lp.services.webapp.publisher import canonical_url
+from lp.services.webapp.menu import structured
+from lp.services.webapp.publisher import (
+    canonical_url,
+    get_current_browser_request,
+    )
 
 
 def product_licenses_modified(product, event):
@@ -50,6 +55,33 @@
             or License.OTHER_OPEN_SOURCE in licenses
             or [License.DONT_KNOW] == licenses)
 
+    def getTemplateName(self):
+        """Return the name of the email template for the licensing case."""
+        licenses = list(self.product.licenses)
+        if [License.DONT_KNOW] == licenses:
+            template_name = 'product-license-dont-know.txt'
+        elif License.OTHER_PROPRIETARY in licenses:
+            template_name = 'product-license-other-proprietary.txt'
+        else:
+            template_name = 'product-license-other-open-source.txt'
+        return template_name
+
+    def getCommercialUseMessage(self):
+        """Return a message explaining the current commercial subscription."""
+        commercial_subscription = self.product.commercial_subscription
+        if commercial_subscription is None:
+            return ''
+        iso_date = commercial_subscription.date_expires.date().isoformat()
+        if not self.product.has_current_commercial_subscription:
+            message = "%s's commercial subscription expired on %s."
+        elif 'complimentary' in commercial_subscription.sales_system_id:
+            message = (
+                "%s's complimentary commercial subscription expires on %s.")
+        else:
+            message = "%s's commercial subscription expires on %s."
+        message = message % (self.product.displayname, iso_date)
+        return textwrap.fill(message, 72)
+
     def send(self):
         """Send a message to the user about the product's license."""
         if not self.needs_notification(self.product):
@@ -61,22 +93,19 @@
             "Launchpad", config.canonical.noreply_from_address)
         commercial_address = format_address(
             'Commercial', 'commercial@xxxxxxxxxxxxx')
-        license_titles = '\n'.join(
-            license.title for license in self.product.licenses)
         substitutions = dict(
-            user_browsername=self.user.displayname,
+            user_displayname=self.user.displayname,
             user_name=self.user.name,
             product_name=self.product.name,
             product_url=canonical_url(self.product),
-            product_summary=self._indent(self.product.summary),
-            license_titles=self._indent(license_titles),
-            license_info=self._indent(self.product.license_info))
+            commercial_use_expiration=self.getCommercialUseMessage(),
+            )
         # Email the user about license policy.
         subject = (
             "License information for %(product_name)s "
             "in Launchpad" % substitutions)
         template = get_email_template(
-            'product-other-license.txt', app='registry')
+            self.getTemplateName(), app='registry')
         message = template % substitutions
         simple_sendmail(
             from_address, user_address,
@@ -85,14 +114,18 @@
         self._addLicenseChangeToReviewWhiteboard()
         return True
 
-    @staticmethod
-    def _indent(text):
-        """Indent the text to be included in the message."""
-        if text is None:
-            return None
-        text = '\n    '.join(line for line in text.split('\n'))
-        text = '    ' + text
-        return text
+    def display(self):
+        """Show a message in a browser page about the product's license."""
+        request = get_current_browser_request()
+        message = self.getCommercialUseMessage()
+        if request is None or message == '':
+            return False
+        safe_message = structured(
+            '%s<br />Learn more about '
+            '<a href="https://help.launchpad.net/CommercialHosting";>'
+            'commercial subscriptions</a>', message)
+        request.response.addNotification(safe_message)
+        return True
 
     @staticmethod
     def _formatDate(now=None):

=== modified file 'lib/lp/registry/tests/test_subscribers.py'
--- lib/lp/registry/tests/test_subscribers.py	2012-03-13 21:09:21 +0000
+++ lib/lp/registry/tests/test_subscribers.py	2012-03-13 21:09:22 +0000
@@ -17,8 +17,10 @@
     LicenseNotification,
     product_licenses_modified,
     )
+from lp.services.webapp.publisher import get_current_browser_request
 from lp.testing import (
     login_person,
+    logout,
     TestCaseWithFactory,
     )
 from lp.testing.layers import DatabaseFunctionalLayer
@@ -145,8 +147,107 @@
         self.assertEqual(1, len(notifications))
         self.verify_user_email(notifications.pop())
 
+    def test_display_no_request(self):
+        # If there is no request, there is no reason to show a message in
+        # the browser.
+        product, user = self.make_product_user([License.GNU_GPL_V2])
+        notification = LicenseNotification(product, user)
+        logout()
+        result = notification.display()
+        self.assertIs(False, result)
+
+    def test_display_no_message(self):
+        # A notification is not added if there is no message to show.
+        product, user = self.make_product_user([License.GNU_GPL_V2])
+        notification = LicenseNotification(product, user)
+        result = notification.display()
+        self.assertEqual('', notification.getCommercialUseMessage())
+        self.assertIs(False, result)
+
+    def test_display_has_message(self):
+        # A notification is added if there is a message to show.
+        product, user = self.make_product_user([License.OTHER_PROPRIETARY])
+        notification = LicenseNotification(product, user)
+        result = notification.display()
+        message = notification.getCommercialUseMessage()
+        self.assertIs(True, result)
+        request = get_current_browser_request()
+        self.assertEqual(1, len(request.response.notifications))
+        self.assertIn(message, request.response.notifications[0].message)
+        self.assertIn(
+            '<a href="https://help.launchpad.net/CommercialHosting";>',
+            request.response.notifications[0].message)
+
+    def test_display_escapee_user_data(self):
+        # A notification is added if there is a message to show.
+        product, user = self.make_product_user([License.OTHER_PROPRIETARY])
+        product.displayname = '<b>Look</b>'
+        notification = LicenseNotification(product, user)
+        result = notification.display()
+        self.assertIs(True, result)
+        request = get_current_browser_request()
+        self.assertEqual(1, len(request.response.notifications))
+        self.assertIn(
+            '&lt;b&gt;Look&lt;/b&gt;',
+            request.response.notifications[0].message)
+
     def test_formatDate(self):
         # Verify the date format.
         now = datetime(2005, 6, 15, 0, 0, 0, 0, pytz.UTC)
         result = LicenseNotification._formatDate(now)
         self.assertEqual('2005-06-15', result)
+
+    def test_getTemplateName_other_dont_know(self):
+        product, user = self.make_product_user([License.DONT_KNOW])
+        notification = LicenseNotification(product, user)
+        self.assertEqual(
+            'product-license-dont-know.txt',
+            notification.getTemplateName())
+
+    def test_getTemplateName_propietary(self):
+        product, user = self.make_product_user([License.OTHER_PROPRIETARY])
+        notification = LicenseNotification(product, user)
+        self.assertEqual(
+            'product-license-other-proprietary.txt',
+            notification.getTemplateName())
+
+    def test_getTemplateName_other_open_source(self):
+        product, user = self.make_product_user([License.OTHER_OPEN_SOURCE])
+        notification = LicenseNotification(product, user)
+        self.assertEqual(
+            'product-license-other-open-source.txt',
+            notification.getTemplateName())
+
+    def test_getCommercialUseMessage_without_commercial_subscription(self):
+        product, user = self.make_product_user([License.MIT])
+        notification = LicenseNotification(product, user)
+        self.assertEqual('', notification.getCommercialUseMessage())
+
+    def test_getCommercialUseMessage_with_complimentary_cs(self):
+        product, user = self.make_product_user([License.OTHER_PROPRIETARY])
+        notification = LicenseNotification(product, user)
+        message = (
+            "Ball's complimentary commercial subscription expires on %s." %
+            product.commercial_subscription.date_expires.date().isoformat())
+        self.assertEqual(message, notification.getCommercialUseMessage())
+
+    def test_getCommercialUseMessage_with_commercial_subscription(self):
+        product, user = self.make_product_user([License.MIT])
+        self.factory.makeCommercialSubscription(product)
+        product.licenses = [License.MIT, License.OTHER_PROPRIETARY]
+        notification = LicenseNotification(product, user)
+        message = (
+            "Ball's commercial subscription expires on %s." %
+            product.commercial_subscription.date_expires.date().isoformat())
+        self.assertEqual(message, notification.getCommercialUseMessage())
+
+    def test_getCommercialUseMessage_with_expired_cs(self):
+        product, user = self.make_product_user([License.MIT])
+        self.factory.makeCommercialSubscription(product, expired=True)
+        product.licenses = [License.MIT, License.OTHER_PROPRIETARY]
+        notification = LicenseNotification(product, user)
+        message = (
+            "Ball's commercial subscription expired on %s." %
+            product.commercial_subscription.date_expires.date().isoformat())
+        self.assertEqual(message, notification.getCommercialUseMessage())
+        self.assertEqual(message, notification.getCommercialUseMessage())