← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~cjwatson/launchpad:distribution-privacy-ui into launchpad:master

 

Colin Watson has proposed merging ~cjwatson/launchpad:distribution-privacy-ui into launchpad:master.

Commit message:
Add UI for distribution privacy

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/+git/launchpad/+merge/416957

We show commercial subscription information if relevant, and Distribution:+admin gains the ability to set the information type.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~cjwatson/launchpad:distribution-privacy-ui into launchpad:master.
diff --git a/lib/lp/app/browser/tests/test_vocabulary.py b/lib/lp/app/browser/tests/test_vocabulary.py
index 8f745ee..644c72d 100644
--- a/lib/lp/app/browser/tests/test_vocabulary.py
+++ b/lib/lp/app/browser/tests/test_vocabulary.py
@@ -444,6 +444,35 @@ class TestDistributionPickerEntrySourceAdapter(TestCaseWithFactory):
             'http://launchpad.test/fnord',
             self.getPickerEntry(distribution).alt_title_link)
 
+    def test_provides_commercial_subscription_none(self):
+        distribution = self.factory.makeDistribution()
+        self.factory.makeDistroSeries(
+            distribution=distribution, status=SeriesStatus.CURRENT)
+        self.assertEqual(
+            'Commercial Subscription: None',
+            self.getPickerEntry(distribution).details[1])
+
+    def test_provides_commercial_subscription_active(self):
+        distribution = self.factory.makeDistribution()
+        self.factory.makeDistroSeries(
+            distribution=distribution, status=SeriesStatus.CURRENT)
+        self.factory.makeCommercialSubscription(distribution)
+        self.assertEqual(
+            'Commercial Subscription: Active',
+            self.getPickerEntry(distribution).details[1])
+
+    def test_provides_commercial_subscription_expired(self):
+        distribution = self.factory.makeDistribution()
+        self.factory.makeDistroSeries(
+            distribution=distribution, status=SeriesStatus.CURRENT)
+        self.factory.makeCommercialSubscription(distribution)
+        then = datetime(2005, 6, 15, 0, 0, 0, 0, pytz.UTC)
+        with celebrity_logged_in('admin'):
+            distribution.commercial_subscription.date_expires = then
+        self.assertEqual(
+            'Commercial Subscription: Expired',
+            self.getPickerEntry(distribution).details[1])
+
 
 @implementer(IHugeVocabulary)
 class TestPersonVocabulary:
diff --git a/lib/lp/app/browser/vocabulary.py b/lib/lp/app/browser/vocabulary.py
index 7cf7eb3..5c1bdab 100644
--- a/lib/lp/app/browser/vocabulary.py
+++ b/lib/lp/app/browser/vocabulary.py
@@ -371,6 +371,16 @@ class DistributionPickerEntrySourceAdapter(TargetPickerEntrySourceAdapter):
         """See `TargetPickerEntrySource`"""
         return target.summary
 
+    def getCommercialSubscription(self, target):
+        """See `TargetPickerEntrySource`"""
+        if target.commercial_subscription:
+            if target.has_current_commercial_subscription:
+                return 'Active'
+            else:
+                return 'Expired'
+        else:
+            return 'None'
+
 
 @adapter(IArchive)
 class ArchivePickerEntrySourceAdapter(DefaultPickerEntrySourceAdapter):
diff --git a/lib/lp/bugs/browser/tests/test_bugtarget_filebug.py b/lib/lp/bugs/browser/tests/test_bugtarget_filebug.py
index 3d4928f..081355f 100644
--- a/lib/lp/bugs/browser/tests/test_bugtarget_filebug.py
+++ b/lib/lp/bugs/browser/tests/test_bugtarget_filebug.py
@@ -461,9 +461,9 @@ class TestFileBugViewBase(FileBugViewMixin, TestCaseWithFactory):
             InformationType.USERDATA, view.default_information_type)
         self.assertEqual(InformationType.USERDATA, bug.information_type)
 
-    def test_filebug_information_type_public_policy(self):
+    def test_filebug_information_type_product_public_policy(self):
         # The vocabulary for information_type when filing a bug is created
-        # correctly for non commercial projects.
+        # correctly for non-commercial projects.
         product = self.factory.makeProduct(official_malone=True)
         with person_logged_in(product.owner):
             view = create_initialized_view(
@@ -472,7 +472,7 @@ class TestFileBugViewBase(FileBugViewMixin, TestCaseWithFactory):
             soup = BeautifulSoup(html)
         self.assertIsNone(soup.find('label', text="Proprietary"))
 
-    def test_filebug_information_type_proprietary_policy(self):
+    def test_filebug_information_type_product_proprietary_policy(self):
         # The vocabulary for information_type when filing a bug is created
         # correctly for a project with a proprietary sharing policy.
         product = self.factory.makeProduct(official_malone=True)
@@ -485,6 +485,32 @@ class TestFileBugViewBase(FileBugViewMixin, TestCaseWithFactory):
             soup = BeautifulSoup(html)
         self.assertIsNotNone(soup.find('label', text="Proprietary"))
 
+    def test_filebug_information_type_distribution_public_policy(self):
+        # The vocabulary for information_type when filing a bug is created
+        # correctly for non-commercial distributions.
+        distribution = self.factory.makeDistribution()
+        removeSecurityProxy(distribution).official_malone = True
+        with person_logged_in(distribution.owner):
+            view = create_initialized_view(
+                distribution, '+filebug', principal=distribution.owner)
+            html = view.render()
+            soup = BeautifulSoup(html)
+        self.assertIsNone(soup.find('label', text="Proprietary"))
+
+    def test_filebug_information_type_distribution_proprietary_policy(self):
+        # The vocabulary for information_type when filing a bug is created
+        # correctly for a distribution with a proprietary sharing policy.
+        distribution = self.factory.makeDistribution()
+        removeSecurityProxy(distribution).official_malone = True
+        self.factory.makeCommercialSubscription(pillar=distribution)
+        with person_logged_in(distribution.owner):
+            distribution.setBugSharingPolicy(BugSharingPolicy.PROPRIETARY)
+            view = create_initialized_view(
+                distribution, '+filebug', principal=distribution.owner)
+            html = view.render()
+            soup = BeautifulSoup(html)
+        self.assertIsNotNone(soup.find('label', text="Proprietary"))
+
     def test_filebug_information_type_vocabulary(self):
         # The vocabulary for information_type when filing a bug is created
         # correctly.
diff --git a/lib/lp/bugs/templates/bugtarget-macros-filebug.pt b/lib/lp/bugs/templates/bugtarget-macros-filebug.pt
index 906664f..6897a86 100644
--- a/lib/lp/bugs/templates/bugtarget-macros-filebug.pt
+++ b/lib/lp/bugs/templates/bugtarget-macros-filebug.pt
@@ -138,13 +138,14 @@
 
 <metal:not_uses_malone define-macro="not_uses_malone">
   <tal:not_uses_malone tal:condition="not: view/contextUsesMalone">
-    <tal:has-context define="product_or_distro view/getProductOrDistroFromContext"
+    <tal:has-context define="product_or_distro view/getProductOrDistroFromContext;
+                             overview_menu context/menu:overview"
                      condition="product_or_distro">
       <div class="highlight-message">
         <a tal:replace="structure product_or_distro/fmt:link">Alsa Utils</a>
         <strong>does not use</strong> Launchpad as its bug tracker.
-        <a tal:attributes="href context/menu:overview/configure_bugtracker/fmt:url"
-           tal:condition="context/required:launchpad.Edit">
+        <a tal:condition="overview_menu/configure_bugtracker/enabled|nothing"
+           tal:attributes="href overview_menu/configure_bugtracker/fmt:url">
           Change this <span class="sprite edit action-icon">Edit</span>
         </a>
       </div>
diff --git a/lib/lp/registry/browser/distribution.py b/lib/lp/registry/browser/distribution.py
index 6ae28d7..f9c5bb0 100644
--- a/lib/lp/registry/browser/distribution.py
+++ b/lib/lp/registry/browser/distribution.py
@@ -60,9 +60,14 @@ from lp.app.browser.launchpadform import (
     )
 from lp.app.browser.lazrjs import InlinePersonEditPickerWidget
 from lp.app.browser.tales import format_link
+from lp.app.enums import PILLAR_INFORMATION_TYPES
 from lp.app.errors import NotFoundError
+from lp.app.vocabularies import InformationTypeVocabulary
 from lp.app.widgets.image import ImageChangeWidget
-from lp.app.widgets.itemswidgets import LabeledMultiCheckBoxWidget
+from lp.app.widgets.itemswidgets import (
+    LabeledMultiCheckBoxWidget,
+    LaunchpadRadioWidgetWithDescription,
+    )
 from lp.archivepublisher.interfaces.publisherconfig import (
     IPublisherConfig,
     IPublisherConfigSet,
@@ -134,6 +139,7 @@ from lp.services.webapp import (
     StandardLaunchpadFacets,
     stepthrough,
     )
+from lp.services.webapp.authorization import check_permission
 from lp.services.webapp.batching import BatchNavigator
 from lp.services.webapp.breadcrumb import Breadcrumb
 from lp.services.webapp.interfaces import ILaunchBag
@@ -798,6 +804,18 @@ class DistributionView(PillarViewMixin, HasAnnouncementsView, FeedsMixin):
         """The 5 most recent derivatives."""
         return self.context.derivatives[:5]
 
+    @cachedproperty
+    def show_commercial_subscription_info(self):
+        """Should subscription information be shown?
+
+        Subscription information is only shown to the distribution owners,
+        Launchpad admins, and members of the Launchpad commercial team.  The
+        first two are allowed via the Launchpad.Edit permission.  The latter
+        is allowed via Launchpad.Commercial.
+        """
+        return (check_permission('launchpad.Edit', self.context) or
+                check_permission('launchpad.Commercial', self.context))
+
 
 class DistributionArchivesView(LaunchpadView):
 
@@ -1090,13 +1108,28 @@ class DistributionAdminView(LaunchpadEditFormView):
         'supports_mirrors',
         'default_traversal_policy',
         'redirect_default_traversal',
+        'information_type',
         ]
 
+    custom_widget_information_type = CustomWidgetFactory(
+        LaunchpadRadioWidgetWithDescription,
+        vocabulary=InformationTypeVocabulary(types=PILLAR_INFORMATION_TYPES))
+
     @property
     def label(self):
         """See `LaunchpadFormView`."""
         return 'Administer %s' % self.context.displayname
 
+    def validate(self, data):
+        super().validate(data)
+        information_type = data.get('information_type')
+        if information_type:
+            errors = [
+                str(e) for e in self.context.checkInformationType(
+                    information_type)]
+            if len(errors) > 0:
+                self.setFieldError('information_type', ' '.join(errors))
+
     @property
     def cancel_url(self):
         return canonical_url(self.context)
diff --git a/lib/lp/registry/browser/tests/distribution-views.txt b/lib/lp/registry/browser/tests/distribution-views.txt
index ef0894d..2407a78 100644
--- a/lib/lp/registry/browser/tests/distribution-views.txt
+++ b/lib/lp/registry/browser/tests/distribution-views.txt
@@ -360,6 +360,52 @@ If the distribution officially uses the application, its portlet does appear.
     portlet-blueprints
 
 
+Displaying commercial subscription information
+----------------------------------------------
+
+Only distribution owners, Launchpad administrators, and Launchpad
+Commercial members are to see commercial subscription information on
+the product overview page.
+
+For distribution owners the property is true.
+
+    >>> from zope.security.proxy import removeSecurityProxy
+
+    >>> commercial_distro = factory.makeDistribution()
+    >>> _ = login_person(removeSecurityProxy(commercial_distro).owner)
+    >>> view = create_initialized_view(commercial_distro, name='+index')
+    >>> print(view.show_commercial_subscription_info)
+    True
+
+For Launchpad admins the property is true.
+
+    >>> login('foo.bar@xxxxxxxxxxxxx')
+    >>> view = create_initialized_view(commercial_distro, name='+index')
+    >>> print(view.show_commercial_subscription_info)
+    True
+
+For Launchpad commercial members the property is true.
+
+    >>> login('commercial-member@xxxxxxxxxxxxx')
+    >>> view = create_initialized_view(commercial_distro, name='+index')
+    >>> print(view.show_commercial_subscription_info)
+    True
+
+But for a no-privileges user the property is false.
+
+    >>> login('no-priv@xxxxxxxxxxxxx')
+    >>> view = create_initialized_view(commercial_distro, name='+index')
+    >>> print(view.show_commercial_subscription_info)
+    False
+
+And for an anonymous user it is false.
+
+    >>> login(ANONYMOUS)
+    >>> view = create_initialized_view(commercial_distro, name='+index')
+    >>> print(view.show_commercial_subscription_info)
+    False
+
+
 Distribution +series
 --------------------
 
diff --git a/lib/lp/registry/browser/tests/product-views.txt b/lib/lp/registry/browser/tests/product-views.txt
index 1c7d580..a644214 100644
--- a/lib/lp/registry/browser/tests/product-views.txt
+++ b/lib/lp/registry/browser/tests/product-views.txt
@@ -91,7 +91,7 @@ For Launchpad admins the property is true.
     >>> print(view.show_commercial_subscription_info)
     True
 
-For Launchpad commercial members th property is true.
+For Launchpad commercial members the property is true.
 
     >>> login('commercial-member@xxxxxxxxxxxxx')
     >>> view = create_initialized_view(firefox, name='+index')
diff --git a/lib/lp/registry/browser/tests/test_distribution_views.py b/lib/lp/registry/browser/tests/test_distribution_views.py
index 252bf72..44ddf0d 100644
--- a/lib/lp/registry/browser/tests/test_distribution_views.py
+++ b/lib/lp/registry/browser/tests/test_distribution_views.py
@@ -6,6 +6,7 @@ from testtools.matchers import MatchesStructure
 import transaction
 from zope.component import getUtility
 
+from lp.app.enums import InformationType
 from lp.archivepublisher.interfaces.publisherconfig import IPublisherConfigSet
 from lp.buildmaster.interfaces.processor import IProcessorSet
 from lp.oci.tests.helpers import OCIConfigHelperMixin
@@ -463,6 +464,7 @@ class TestDistributionAdminView(TestCaseWithFactory):
                 'field.supports_mirrors': 'on',
                 'field.default_traversal_policy': 'SERIES',
                 'field.redirect_default_traversal': 'on',
+                'field.information_type': 'PUBLIC',
                 'field.actions.change': 'change'})
         self.assertThat(
             distribution,
@@ -471,7 +473,8 @@ class TestDistributionAdminView(TestCaseWithFactory):
                 supports_mirrors=True,
                 default_traversal_policy=(
                     DistributionDefaultTraversalPolicy.SERIES),
-                redirect_default_traversal=True))
+                redirect_default_traversal=True,
+                information_type=InformationType.PUBLIC))
         create_initialized_view(
             distribution, '+admin', principal=admin,
             form={
@@ -479,6 +482,7 @@ class TestDistributionAdminView(TestCaseWithFactory):
                 'field.supports_mirrors': '',
                 'field.default_traversal_policy': 'OCI_PROJECT',
                 'field.redirect_default_traversal': '',
+                'field.information_type': 'PROPRIETARY',
                 'field.actions.change': 'change'})
         self.assertThat(
             distribution,
@@ -487,7 +491,8 @@ class TestDistributionAdminView(TestCaseWithFactory):
                 supports_mirrors=False,
                 default_traversal_policy=(
                     DistributionDefaultTraversalPolicy.OCI_PROJECT),
-                redirect_default_traversal=False))
+                redirect_default_traversal=False,
+                information_type=InformationType.PROPRIETARY))
 
 
 class TestDistroReassignView(TestCaseWithFactory):
diff --git a/lib/lp/registry/interfaces/distribution.py b/lib/lp/registry/interfaces/distribution.py
index 2e3a406..b0bdc85 100644
--- a/lib/lp/registry/interfaces/distribution.py
+++ b/lib/lp/registry/interfaces/distribution.py
@@ -466,6 +466,12 @@ class IDistributionView(
             "An object which contains the timeframe and the voucher code of a "
             "subscription.")))
 
+    commercial_subscription_is_due = exported(Bool(
+        title=_("Commercial subscription is due"), readonly=True,
+        description=_(
+            "Whether the distribution's licensing requires a new commercial "
+            "subscription to use launchpad.")))
+
     has_current_commercial_subscription = Attribute(
         "Whether the distribution has a current commercial subscription.")
 
diff --git a/lib/lp/registry/model/distribution.py b/lib/lp/registry/model/distribution.py
index ff56b03..26431e1 100644
--- a/lib/lp/registry/model/distribution.py
+++ b/lib/lp/registry/model/distribution.py
@@ -519,6 +519,29 @@ class Distribution(SQLBase, BugTargetBase, MakesAnnouncements,
         return (self.commercial_subscription
             and self.commercial_subscription.date_expires > now)
 
+    @property
+    def commercial_subscription_is_due(self):
+        """See `IDistribution`.
+
+        If True, display subscription warning to distribution owner.
+        """
+        if self.information_type not in PROPRIETARY_INFORMATION_TYPES:
+            return False
+        elif (self.commercial_subscription is None
+              or not self.commercial_subscription.is_active):
+            # The distribution doesn't have an active subscription.
+            return True
+        else:
+            warning_date = (self.commercial_subscription.date_expires
+                            - timedelta(30))
+            now = datetime.now(pytz.UTC)
+            if now > warning_date:
+                # The subscription is close to being expired.
+                return True
+            else:
+                # The subscription is good.
+                return False
+
     def _ensure_complimentary_subscription(self):
         """Create a complementary commercial subscription for the distro."""
         if not self.commercial_subscription:
diff --git a/lib/lp/registry/stories/webservice/xx-distribution.txt b/lib/lp/registry/stories/webservice/xx-distribution.txt
index 0e64400..d33602c 100644
--- a/lib/lp/registry/stories/webservice/xx-distribution.txt
+++ b/lib/lp/registry/stories/webservice/xx-distribution.txt
@@ -28,6 +28,7 @@ And for every distribution we publish most of its attributes.
     bug_reporting_guidelines: None
     bug_supervisor_link: None
     cdimage_mirrors_collection_link: 'http://.../ubuntu/cdimage_mirrors'
+    commercial_subscription_is_due: False
     commercial_subscription_link: None
     current_series_link: 'http://.../ubuntu/hoary'
     date_created: '2006-10-16T18:31:43.415195+00:00'
diff --git a/lib/lp/registry/templates/distribution-index.pt b/lib/lp/registry/templates/distribution-index.pt
index 1c7c7bd..794b28c 100644
--- a/lib/lp/registry/templates/distribution-index.pt
+++ b/lib/lp/registry/templates/distribution-index.pt
@@ -33,6 +33,14 @@
     <tal:main metal:fill-slot="main"
       define="overview_menu context/menu:overview">
       <div class="top-portlet">
+        <tal:warning-for-owner
+          condition="view/show_commercial_subscription_info">
+          <div
+            style="border-bottom: 1px solid #EBEBEB; margin-bottom: 1em;"
+            tal:condition="context/commercial_subscription_is_due"
+            tal:content="structure context/@@+portlet-requires-subscription"/>
+        </tal:warning-for-owner>
+
         <div
           class="summary"
           tal:content="structure context/summary/fmt:text-to-html" />