← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~deryck/launchpad/milestone-information-type into lp:launchpad

 

Deryck Hodge has proposed merging lp:~deryck/launchpad/milestone-information-type into lp:launchpad.

Commit message:
Make Milestone gets its information_type from its related Product. (Part of private projects work.)

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~deryck/launchpad/milestone-information-type/+merge/128227

This branch adapts Milestone to Product for IInformationType so that Milestone has its related Product's information_type.  Abel's excellent security adapter work will secure Milestone once it provides IInformationType.  So this branch does the adaptation and deals with the remaining UI work for the web UI.

I discussed the approach here with abentley on a couple different occasions.

The primary work is to provide an adapter.  I have also added two tests for that. One to check the adapter function itself, and another to ensure that Milestone has IInformationType.  The rest of the work is adding the privacy portlet UI.  I did not add the edit icon for this because this UI should informational.  One could even argue it's un-needed since the privacy banner works, but I added it to be consistent with other pages.  The main work for this part was updating the InformationTypePortletMixin to also handle an adapted object.  The rest is just templating and zcml to wire up the portlet and a few tests.

I also added information_type_description to LaunchpadView as a small add-on fix.  LaunchpadView is providing information_type, so it made since to me to also add the description.

I have a few tests I'm fixing but I thought it worth getting this up for review now.  We are lint free, too.
-- 
https://code.launchpad.net/~deryck/launchpad/milestone-information-type/+merge/128227
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~deryck/launchpad/milestone-information-type into lp:launchpad.
=== modified file 'lib/lp/app/browser/informationtype.py'
--- lib/lp/app/browser/informationtype.py	2012-09-17 15:19:10 +0000
+++ lib/lp/app/browser/informationtype.py	2012-10-05 11:22:24 +0000
@@ -9,28 +9,51 @@
 from lazr.restful.interfaces import IJSONRequestCache
 
 from lp.app.enums import PRIVATE_INFORMATION_TYPES
+from lp.app.interfaces.informationtype import IInformationType
 from lp.app.utilities import json_dump_information_types
 
 
 class InformationTypePortletMixin:
 
     def initialize(self):
+        information_typed = IInformationType(self.context, None)
+        if information_typed is None:
+            information_typed = self.context
         cache = IJSONRequestCache(self.request)
         json_dump_information_types(
             cache,
-            self.context.getAllowedInformationTypes(self.user))
+            information_typed.getAllowedInformationTypes(self.user))
 
     @property
     def information_type(self):
-        return self.context.information_type.title
+        information_typed = IInformationType(self.context, None)
+        if information_typed is None:
+            information_typed = self.context
+        return information_typed.information_type.title
 
     @property
     def information_type_description(self):
-        return self.context.information_type.description
+        information_typed = IInformationType(self.context, None)
+        if information_typed is None:
+            information_typed = self.context
+        return information_typed.information_type.description
 
     @property
     def information_type_css(self):
-        if self.context.information_type in PRIVATE_INFORMATION_TYPES:
+        information_typed = IInformationType(self.context, None)
+        if information_typed is None:
+            information_typed = self.context
+        if information_typed.information_type in PRIVATE_INFORMATION_TYPES:
             return 'sprite private'
         else:
             return 'sprite public'
+
+    @property
+    def privacy_portlet_css(self):
+        information_typed = IInformationType(self.context, None)
+        if information_typed is None:
+            information_typed = self.context
+        if information_typed.information_type in PRIVATE_INFORMATION_TYPES:
+            return 'portlet private'
+        else:
+            return 'portlet public'

=== modified file 'lib/lp/registry/adapters.py'
--- lib/lp/registry/adapters.py	2012-01-01 02:58:52 +0000
+++ lib/lp/registry/adapters.py	2012-10-05 11:22:24 +0000
@@ -133,3 +133,8 @@
 def package_to_sourcepackagename(package):
     """Adapts a package to its `ISourcePackageName`."""
     return package.sourcepackagename
+
+
+def information_type_from_product(milestone):
+    """Adapts a milestone to product for information_type."""
+    return milestone.product

=== modified file 'lib/lp/registry/browser/configure.zcml'
--- lib/lp/registry/browser/configure.zcml	2012-08-15 16:05:54 +0000
+++ lib/lp/registry/browser/configure.zcml	2012-10-05 11:22:24 +0000
@@ -1294,6 +1294,9 @@
         <browser:page
             name="+productrelease-data"
             template="../templates/productrelease-portlet-data.pt"/>
+        <browser:page
+            name="+portlet-privacy"
+            template="../templates/milestone-portlet-privacy.pt"/>
     </browser:pages>
     <browser:page
         name="+pillar-table-row"

=== modified file 'lib/lp/registry/browser/milestone.py'
--- lib/lp/registry/browser/milestone.py	2012-06-11 00:03:25 +0000
+++ lib/lp/registry/browser/milestone.py	2012-10-05 11:22:24 +0000
@@ -38,6 +38,7 @@
     )
 
 from lp import _
+from lp.app.browser.informationtype import InformationTypePortletMixin
 from lp.app.browser.launchpadform import (
     action,
     custom_widget,
@@ -355,7 +356,8 @@
 
 
 class MilestoneView(
-    LaunchpadView, MilestoneViewMixin, ProductDownloadFileMixin):
+    LaunchpadView, MilestoneViewMixin, ProductDownloadFileMixin,
+    InformationTypePortletMixin):
     """A View for listing milestones and releases."""
     # XXX sinzui 2009-05-29 bug=381672: Extract the BugTaskListingItem rules
     # to a mixin so that MilestoneView and others can use it.

=== modified file 'lib/lp/registry/browser/tests/test_milestone.py'
--- lib/lp/registry/browser/tests/test_milestone.py	2012-10-02 21:23:52 +0000
+++ lib/lp/registry/browser/tests/test_milestone.py	2012-10-05 11:22:24 +0000
@@ -5,8 +5,10 @@
 
 __metaclass__ = type
 
+import soupmatchers
 from testtools.matchers import LessThan
 from zope.component import getUtility
+from zope.security.proxy import removeSecurityProxy
 
 from lp.app.enums import InformationType
 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
@@ -20,6 +22,7 @@
 from lp.registry.model.milestonetag import ProjectGroupMilestoneTag
 from lp.services.webapp import canonical_url
 from lp.testing import (
+    BrowserTestCase,
     login_person,
     login_team,
     logout,
@@ -36,7 +39,7 @@
 from lp.testing.views import create_initialized_view
 
 
-class TestMilestoneViews(TestCaseWithFactory):
+class TestMilestoneViews(BrowserTestCase):
 
     layer = DatabaseFunctionalLayer
 
@@ -54,6 +57,52 @@
         view = create_initialized_view(milestone, '+index')
         self.assertIn('some work for this milestone', view.render())
 
+    def test_information_type_public(self):
+        # A milestone's view should include its information_type,
+        # which defaults to Public for new projects.
+        milestone = self.factory.makeMilestone()
+        view = create_initialized_view(milestone, '+index')
+        self.assertEqual('Public', view.information_type)
+
+    def test_information_type_proprietary(self):
+        # A milestone's view should get its information_type
+        # from the related product even if the product is changed to
+        # PROPRIETARY.
+        product = self.factory.makeProduct()
+        self.factory.makeCommercialSubscription(product)
+        information_type = InformationType.PROPRIETARY
+        removeSecurityProxy(product).information_type = information_type
+        milestone = self.factory.makeMilestone(product=product)
+        view = create_initialized_view(milestone, '+index')
+        self.assertEqual('Proprietary', view.information_type)
+
+    def test_privacy_portlet(self):
+        # A milestone's page should include a privacy portlet that
+        # accurately describes the information_type.
+        owner = self.factory.makePerson()
+        product = self.factory.makeProduct(owner=owner)
+        self.factory.makeCommercialSubscription(product)
+        information_type = InformationType.PROPRIETARY
+        removeSecurityProxy(product).information_type = information_type
+        milestone = self.factory.makeMilestone(product=product)
+        policy = self.factory.makeAccessPolicy(pillar=product)
+        grant = self.factory.makeAccessPolicyGrant(
+            policy=policy, grantee=owner)
+        privacy_portlet = soupmatchers.Tag(
+            'info-type-portlet', 'span',
+            attrs={'id': 'information-type-summary'})
+        privacy_portlet_proprietary = soupmatchers.Tag(
+            'info-type-text', 'strong', attrs={'id': 'information-type'},
+            text='Proprietary')
+        browser = self.getViewBrowser(milestone, '+index', user=owner)
+        # First, assert that the portlet exists.
+        self.assertThat(
+            browser.contents, soupmatchers.HTMLContains(privacy_portlet))
+        # Then, assert that the text displayed matches the information_type.
+        self.assertThat(
+            browser.contents, soupmatchers.HTMLContains(
+            privacy_portlet_proprietary))
+
 
 class TestAddMilestoneViews(TestCaseWithFactory):
 

=== modified file 'lib/lp/registry/configure.zcml'
--- lib/lp/registry/configure.zcml	2012-10-03 18:35:09 +0000
+++ lib/lp/registry/configure.zcml	2012-10-05 11:22:24 +0000
@@ -1077,6 +1077,10 @@
             interface="lp.bugs.interfaces.structuralsubscription.IStructuralSubscriptionTargetWrite" />
 
     </class>
+    <adapter
+        provides="lp.app.interfaces.informationtype.IInformationType"
+        for="lp.registry.interfaces.milestone.IMilestone"
+        factory="lp.registry.adapters.information_type_from_product"/>
 
     <!-- IMilestoneSet -->
 

=== modified file 'lib/lp/registry/templates/milestone-index.pt'
--- lib/lp/registry/templates/milestone-index.pt	2012-07-10 10:33:09 +0000
+++ lib/lp/registry/templates/milestone-index.pt	2012-10-05 11:22:24 +0000
@@ -340,6 +340,7 @@
 
     <tal:side metal:fill-slot="side">
       <tal:menu replace="structure view/milestone/@@+global-actions" />
+      <tal:privacy replace="structure context/@@+portlet-privacy" />
     </tal:side>
   </body>
 </html>

=== added file 'lib/lp/registry/templates/milestone-portlet-privacy.pt'
--- lib/lp/registry/templates/milestone-portlet-privacy.pt	1970-01-01 00:00:00 +0000
+++ lib/lp/registry/templates/milestone-portlet-privacy.pt	2012-10-05 11:22:24 +0000
@@ -0,0 +1,15 @@
+<div
+  xmlns:tal="http://xml.zope.org/namespaces/tal";
+  xmlns:metal="http://xml.zope.org/namespaces/metal";
+  xmlns:i18n="http://xml.zope.org/namespaces/i18n";
+  id="privacy"
+  tal:attributes="class view/privacy_portlet_css"
+>
+    <span id="information-type-summary"
+      tal:attributes="class view/information_type_css;">This milestone
+      contains <strong id="information-type" tal:content="view/information_type">
+    </strong> information</span>
+
+    <div id="information-type-description" style="padding-top: 5px" tal:content="view/information_type_description"></div>
+</div>
+

=== modified file 'lib/lp/registry/tests/test_adapters.py'
--- lib/lp/registry/tests/test_adapters.py	2012-01-01 02:58:52 +0000
+++ lib/lp/registry/tests/test_adapters.py	2012-10-05 11:22:24 +0000
@@ -10,6 +10,7 @@
     package_to_sourcepackagename,
     productseries_to_product,
     sourcepackage_to_distribution,
+    information_type_from_product,
     )
 from lp.registry.interfaces.distribution import IDistribution
 from lp.registry.interfaces.product import IProduct
@@ -73,3 +74,11 @@
         self.assertTrue(IProduct.providedBy(product))
         self.assertEqual(product_series.product, product)
         self.assertEqual(product, IProduct(product_series))
+
+    def test_information_type_from_product(self):
+        # information_type_from_product() returns an IProduct given
+        # an IMilestone.
+        milestone = self.factory.makeMilestone()
+        product = information_type_from_product(milestone)
+        self.assertTrue(IProduct.providedBy(product))
+        self.assertEqual(product, milestone.product)

=== modified file 'lib/lp/registry/tests/test_milestone.py'
--- lib/lp/registry/tests/test_milestone.py	2012-10-02 02:56:32 +0000
+++ lib/lp/registry/tests/test_milestone.py	2012-10-05 11:22:24 +0000
@@ -9,8 +9,11 @@
 import unittest
 
 from zope.component import getUtility
+from zope.security.proxy import removeSecurityProxy
 
+from lp.app.enums import InformationType
 from lp.app.errors import NotFoundError
+from lp.app.interfaces.informationtype import IInformationType
 from lp.registry.interfaces.distribution import IDistributionSet
 from lp.registry.interfaces.milestone import (
     IHasMilestones,
@@ -231,3 +234,19 @@
         self.factory.makeSpecificationWorkItem(
             specification=specification, milestone=milestone, deleted=True)
         self.assertContentEqual([], milestone.specifications)
+
+
+class TestMilestoneInformationType(TestCaseWithFactory):
+    """Tests for information_type and Milestone."""
+
+    layer = DatabaseFunctionalLayer
+
+    def test_information_type_from_product(self):
+        # Milestones should inherit information_type from its product."""
+        product = self.factory.makeProduct()
+        self.factory.makeCommercialSubscription(product)
+        information_type = InformationType.PROPRIETARY
+        removeSecurityProxy(product).information_type = information_type
+        milestone = self.factory.makeMilestone(product=product)
+        self.assertEqual(
+            IInformationType(milestone).information_type, information_type)

=== modified file 'lib/lp/services/webapp/publisher.py'
--- lib/lp/services/webapp/publisher.py	2012-09-18 19:41:02 +0000
+++ lib/lp/services/webapp/publisher.py	2012-10-05 11:22:24 +0000
@@ -310,6 +310,14 @@
             return None
         return information_typed.information_type.title
 
+    @property
+    def information_type_description(self):
+        """A view has the information_type_description of its context."""
+        information_typed = IInformationType(self.context, None)
+        if information_typed is None:
+            return None
+        return information_typed.information_type.description
+
     def __init__(self, context, request):
         self.context = context
         self.request = request


Follow ups