launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #28225
[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" />