launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #14653
[Merge] lp:~abentley/launchpad/no-private-translations into lp:launchpad
Aaron Bentley has proposed merging lp:~abentley/launchpad/no-private-translations into lp:launchpad.
Commit message:
Prevent translations for private products.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
Related bugs:
Bug #1083199 in Launchpad itself: "translations can be enabled for private projects"
https://bugs.launchpad.net/launchpad/+bug/1083199
For more details, see:
https://code.launchpad.net/~abentley/launchpad/no-private-translations/+merge/137316
= Summary =
Fix bug #1083199: translations can be enabled for private projects
== Proposed fix ==
Fix issues at the model and view level
== Pre-implementation notes ==
None
== LOC Rationale ==
Part of Private Projects
== Implementation details ==
Partial rollback of r16196 because it assumes that private products can have translations. (This is the getTranslatables filtering)
=== Model level ===
Make it impossible to make a product if:
- it has queued translations
- it has any productseries with autoimports enabled
(This validation also applies to the view)
Introduce a Storm validator for Product.translations_usage that rejects ServiceUsage.LAUNCHPAD for private projects.
Introduce a Storm validator for ProductSeries.autoimport_mode that permits only
TranslationsBranchImportMode.NO_IMPORT for ProductSeries of private products.
=== View level ===
Remove ServiceUsage.LAUNCHPAD from the list of available translations_usage settings (just like answers).
Hide links to the productseries translation settings for private products.
Remove import/export settings for private productseries.
== Tests ==
bin/test -t test_private_disables_imports -t test_private_disables_imports -t est_launchpad_not_listed_for_proprietary -t test_private_forbids_autoimport -t test_private_forbids_translations -t test_checkInformationType_auto_translation_imports -t test_checkInformationType_queued_translations
== Demo and Q/A ==
- Create a product.
- Enable translations.
- Enable automatic imports to trunk.
- Attempt to set the product to proprietary.
- You should receive errors about the fact that translations and auto-imports are enabled.
- Disable translations and automatic imports.
- Set the product to proprietary.
- You should no longer have the option to enable translations.
- The links to the productseries pages should be gone.
- If you URL-hack to the productseries pages, the UI should be gone.
= Launchpad lint =
Checking for conflicts and issues in changed files.
Linting changed files:
lib/lp/translations/browser/tests/test_productseries.py
lib/lp/registry/tests/test_productseries.py
lib/lp/registry/model/productseries.py
lib/lp/registry/browser/product.py
lib/lp/translations/templates/productseries-translations-settings.pt
lib/lp/translations/browser/product.py
lib/lp/registry/model/product.py
lib/lp/translations/templates/product-portlet-translatables.pt
lib/lp/translations/browser/tests/test_product_view.py
lib/lp/registry/tests/test_product.py
--
https://code.launchpad.net/~abentley/launchpad/no-private-translations/+merge/137316
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~abentley/launchpad/no-private-translations into lp:launchpad.
=== modified file 'lib/lp/registry/browser/product.py'
--- lib/lp/registry/browser/product.py 2012-11-27 15:04:39 +0000
+++ lib/lp/registry/browser/product.py 2012-11-30 20:20:39 +0000
@@ -1332,7 +1332,8 @@
field.description = (
field.description.replace('pillar', 'project'))
usage_field.field = field
- if (self.usage_fieldname == 'answers_usage' and
+ if (self.usage_fieldname in
+ ('answers_usage', 'translations_usage') and
self.context.information_type in
PROPRIETARY_INFORMATION_TYPES):
values = usage_field.field.vocabulary.items
=== modified file 'lib/lp/registry/model/product.py'
--- lib/lp/registry/model/product.py 2012-11-29 19:29:15 +0000
+++ lib/lp/registry/model/product.py 2012-11-30 20:20:39 +0000
@@ -212,6 +212,8 @@
from lp.translations.interfaces.customlanguagecode import (
IHasCustomLanguageCodes,
)
+from lp.translations.interfaces.translations import (
+ TranslationsBranchImportMode)
from lp.translations.model.customlanguagecode import (
CustomLanguageCode,
HasCustomLanguageCodesMixin,
@@ -507,6 +509,17 @@
POTemplate.productseries == ProductSeries.id)
if not templates.is_empty():
yield CannotChangeInformationType('This project has translations.')
+ if not self.getTranslationImportQueueEntries().is_empty():
+ yield CannotChangeInformationType(
+ 'This project has queued translations.')
+ import_productseries = store.find(
+ ProductSeries,
+ ProductSeries.product == self.id,
+ ProductSeries.translations_autoimport_mode !=
+ TranslationsBranchImportMode.NO_IMPORT)
+ if not import_productseries.is_empty():
+ yield CannotChangeInformationType(
+ 'Some product series have translation imports enabled.')
if not self.packagings.is_empty():
yield CannotChangeInformationType('Some series are packaged.')
if self.translations_usage == ServiceUsage.LAUNCHPAD:
@@ -592,10 +605,18 @@
dbName="blueprints_usage", notNull=True,
schema=ServiceUsage,
default=ServiceUsage.UNKNOWN)
+
+ def validate_translations_usage(self, attr, value):
+ if value == ServiceUsage.LAUNCHPAD and self.private:
+ raise ProprietaryProduct(
+ "Translations are not supported for proprietary products.")
+ return value
+
translations_usage = EnumCol(
dbName="translations_usage", notNull=True,
schema=ServiceUsage,
- default=ServiceUsage.UNKNOWN)
+ default=ServiceUsage.UNKNOWN,
+ storm_validator=validate_translations_usage)
@property
def codehosting_usage(self):
@@ -2072,16 +2093,13 @@
def getTranslatables(self):
"""See `IProductSet`"""
- user = getUtility(ILaunchBag).user
- privacy_clause = self.getProductPrivacyFilter(user)
results = IStore(Product).find(
(Product, Person),
Product.active == True,
Product.id == ProductSeries.productID,
POTemplate.productseriesID == ProductSeries.id,
Product.translations_usage == ServiceUsage.LAUNCHPAD,
- Person.id == Product._ownerID,
- privacy_clause).config(
+ Person.id == Product._ownerID).config(
distinct=True).order_by(Product.title)
# We only want Product - the other tables are just to populate
=== modified file 'lib/lp/registry/model/productseries.py'
--- lib/lp/registry/model/productseries.py 2012-10-18 14:05:41 +0000
+++ lib/lp/registry/model/productseries.py 2012-11-30 20:20:39 +0000
@@ -58,6 +58,7 @@
from lp.bugs.model.structuralsubscription import (
StructuralSubscriptionTargetMixin,
)
+from lp.registry.errors import ProprietaryProduct
from lp.registry.interfaces.packaging import PackagingType
from lp.registry.interfaces.person import validate_person
from lp.registry.interfaces.productrelease import IProductReleaseSet
@@ -147,11 +148,23 @@
notNull=False, default=None)
branch = ForeignKey(foreignKey='Branch', dbName='branch',
default=None)
+
+ def validate_autoimport_mode(self, attr, value):
+ # Perform the normal validation for None
+ if value is None:
+ return value
+ if (self.product.private and
+ value != TranslationsBranchImportMode.NO_IMPORT):
+ raise ProprietaryProduct('Translations are disabled for'
+ ' private projects.')
+ return value
+
translations_autoimport_mode = EnumCol(
dbName='translations_autoimport_mode',
notNull=True,
schema=TranslationsBranchImportMode,
- default=TranslationsBranchImportMode.NO_IMPORT)
+ default=TranslationsBranchImportMode.NO_IMPORT,
+ storm_validator=validate_autoimport_mode)
translations_branch = ForeignKey(
dbName='translations_branch', foreignKey='Branch', notNull=False,
default=None)
=== modified file 'lib/lp/registry/tests/test_product.py'
--- lib/lp/registry/tests/test_product.py 2012-11-26 21:09:20 +0000
+++ lib/lp/registry/tests/test_product.py 2012-11-30 20:20:39 +0000
@@ -94,7 +94,6 @@
from lp.services.features.testing import FeatureFixture
from lp.services.webapp.authorization import check_permission
from lp.testing import (
- ANONYMOUS,
celebrity_logged_in,
login,
person_logged_in,
@@ -122,12 +121,14 @@
from lp.translations.interfaces.customlanguagecode import (
IHasCustomLanguageCodes,
)
+from lp.translations.interfaces.translations import (
+ TranslationsBranchImportMode)
class TestProduct(TestCaseWithFactory):
"""Tests product object."""
- layer = DatabaseFunctionalLayer
+ layer = LaunchpadFunctionalLayer
def test_pillar_category(self):
# Products are really called Projects
@@ -440,6 +441,7 @@
licenses=[License.OTHER_PROPRIETARY])
with person_logged_in(product.owner):
for usage in ServiceUsage:
+ product.information_type = InformationType.PUBLIC
product.translations_usage = usage.value
for info_type in PROPRIETARY_INFORMATION_TYPES:
if product.translations_usage == ServiceUsage.LAUNCHPAD:
@@ -548,6 +550,59 @@
'This project has translations.'):
raise error
+ def test_checkInformationType_queued_translations(self):
+ # Proprietary products must not have queued translations
+ productseries = self.factory.makeProductSeries()
+ product = productseries.product
+ entry = self.factory.makeTranslationImportQueueEntry(
+ productseries=productseries)
+ for info_type in PROPRIETARY_INFORMATION_TYPES:
+ with person_logged_in(product.owner):
+ error, = list(product.checkInformationType(info_type))
+ with ExpectedException(CannotChangeInformationType,
+ 'This project has queued translations.'):
+ raise error
+ removeSecurityProxy(entry).delete(entry.id)
+ with person_logged_in(product.owner):
+ for info_type in PROPRIETARY_INFORMATION_TYPES:
+ self.assertContentEqual(
+ [], product.checkInformationType(info_type))
+
+ def test_checkInformationType_auto_translation_imports(self):
+ # Proprietary products must not be at risk of creating translations.
+ productseries = self.factory.makeProductSeries()
+ product = productseries.product
+ self.useContext(person_logged_in(product.owner))
+ for mode in TranslationsBranchImportMode.items:
+ if mode == TranslationsBranchImportMode.NO_IMPORT:
+ continue
+ productseries.translations_autoimport_mode = mode
+ for info_type in PROPRIETARY_INFORMATION_TYPES:
+ error, = list(product.checkInformationType(info_type))
+ with ExpectedException(CannotChangeInformationType,
+ 'Some product series have translation imports enabled.'):
+ raise error
+ productseries.translations_autoimport_mode = (
+ TranslationsBranchImportMode.NO_IMPORT)
+ for info_type in PROPRIETARY_INFORMATION_TYPES:
+ self.assertContentEqual(
+ [], product.checkInformationType(info_type))
+
+ def test_private_forbids_translations(self):
+ owner = self.factory.makePerson()
+ product = self.factory.makeProduct(owner=owner)
+ self.useContext(person_logged_in(owner))
+ for info_type in PROPRIETARY_INFORMATION_TYPES:
+ product.information_type = info_type
+ with ExpectedException(
+ ProprietaryProduct,
+ "Translations are not supported for proprietary products."):
+ product.translations_usage = ServiceUsage.LAUNCHPAD
+ for usage in ServiceUsage.items:
+ if usage == ServiceUsage.LAUNCHPAD:
+ continue
+ product.translations_usage = usage
+
def createProduct(self, information_type=None, license=None):
# convenience method for testing IProductSet.createProduct rather than
# self.factory.makeProduct
@@ -2248,34 +2303,6 @@
self.assertIn(embargoed, result)
self.assertIn(proprietary, result)
- def test_getTranslatables_filters_private_products(self):
- # ProductSet.getTranslatables() returns private translatable
- # products only for user that have grants for these products.
- owner = self.factory.makePerson()
- product = self.factory.makeProduct(
- owner=owner, translations_usage=ServiceUsage.LAUNCHPAD,
- information_type=InformationType.PROPRIETARY)
- series = self.factory.makeProductSeries(product)
- with person_logged_in(owner):
- self.factory.makePOTemplate(productseries=series)
- # Anonymous users do not see private products.
- with person_logged_in(ANONYMOUS):
- translatables = getUtility(IProductSet).getTranslatables()
- self.assertNotIn(product, list(translatables))
- # Ordinary users do not see private products.
- user = self.factory.makePerson()
- with person_logged_in(user):
- translatables = getUtility(IProductSet).getTranslatables()
- self.assertNotIn(product, list(translatables))
- # Users with policy grants on private products see them.
- with person_logged_in(owner):
- getUtility(IService, 'sharing').sharePillarInformation(
- product, user, owner,
- {InformationType.PROPRIETARY: SharingPermission.ALL})
- with person_logged_in(user):
- translatables = getUtility(IProductSet).getTranslatables()
- self.assertIn(product, list(translatables))
-
class TestProductSetWebService(WebServiceTestCase):
=== modified file 'lib/lp/registry/tests/test_productseries.py'
--- lib/lp/registry/tests/test_productseries.py 2012-11-27 22:02:34 +0000
+++ lib/lp/registry/tests/test_productseries.py 2012-11-30 20:20:39 +0000
@@ -16,11 +16,17 @@
from zope.security.interfaces import Unauthorized
from zope.security.proxy import removeSecurityProxy
-from lp.app.enums import InformationType
+from lp.app.enums import (
+ InformationType,
+ PROPRIETARY_INFORMATION_TYPES,
+ )
from lp.app.interfaces.informationtype import IInformationType
from lp.app.interfaces.services import IService
from lp.registry.enums import SharingPermission
-from lp.registry.errors import CannotPackageProprietaryProduct
+from lp.registry.errors import (
+ CannotPackageProprietaryProduct,
+ ProprietaryProduct,
+ )
from lp.registry.interfaces.distribution import IDistributionSet
from lp.registry.interfaces.distroseries import IDistroSeriesSet
from lp.registry.interfaces.productseries import (
@@ -64,6 +70,19 @@
self.assertEqual(
IInformationType(series).information_type, information_type)
+ def test_private_forbids_autoimport(self):
+ # Autoimports are forbidden if products are proprietary/embargoed.
+ series = self.factory.makeProductSeries()
+ self.useContext(person_logged_in(series.product.owner))
+ for info_type in PROPRIETARY_INFORMATION_TYPES:
+ series.product.information_type = info_type
+ for mode in TranslationsBranchImportMode.items:
+ if mode == TranslationsBranchImportMode.NO_IMPORT:
+ continue
+ with ExpectedException(ProprietaryProduct,
+ 'Translations are disabled for private projects.'):
+ series.translations_autoimport_mode = mode
+
class ProductSeriesReleasesTestCase(TestCaseWithFactory):
"""Test for ProductSeries.release property."""
=== modified file 'lib/lp/translations/browser/product.py'
--- lib/lp/translations/browser/product.py 2012-01-01 02:58:52 +0000
+++ lib/lp/translations/browser/product.py 2012-11-30 20:20:39 +0000
@@ -136,3 +136,8 @@
return [series for series in self.context.series if (
series.status != SeriesStatus.OBSOLETE and
series not in translatable)]
+
+ @property
+ def allow_series_translation(self):
+ return (check_permission("launchpad.Edit", self.context) and not
+ self.context.private)
=== modified file 'lib/lp/translations/browser/tests/test_product_view.py'
--- lib/lp/translations/browser/tests/test_product_view.py 2012-01-01 02:58:52 +0000
+++ lib/lp/translations/browser/tests/test_product_view.py 2012-11-30 20:20:39 +0000
@@ -3,12 +3,25 @@
__metaclass__ = type
-from lp.app.enums import ServiceUsage
+
+from soupmatchers import (
+ HTMLContains,
+ Tag,
+)
+from testtools.matchers import Not
+
+from lp.app.enums import (
+ InformationType,
+ PUBLIC_PROPRIETARY_INFORMATION_TYPES,
+ ServiceUsage,
+ )
from lp.registry.interfaces.series import SeriesStatus
+from lp.services.webapp import canonical_url
from lp.services.webapp.servers import LaunchpadTestRequest
from lp.testing import (
celebrity_logged_in,
login_person,
+ person_logged_in,
TestCaseWithFactory,
)
from lp.testing.layers import (
@@ -16,6 +29,7 @@
LaunchpadZopelessLayer,
)
from lp.testing.views import create_view
+from lp.testing.views import create_initialized_view
from lp.translations.browser.product import ProductView
from lp.translations.publisher import TranslationsLayer
@@ -114,3 +128,58 @@
view = create_view(product, '+translations',
layer=TranslationsLayer)
self.assertEqual(True, view.can_configure_translations())
+
+ def test_launchpad_not_listed_for_proprietary(self):
+ product = self.factory.makeProduct()
+ with person_logged_in(product.owner):
+ for info_type in PUBLIC_PROPRIETARY_INFORMATION_TYPES:
+ product.information_type = info_type
+ view = create_initialized_view(
+ product, '+configure-translations',
+ layer=TranslationsLayer)
+ if product.private:
+ self.assertNotIn(
+ ServiceUsage.LAUNCHPAD,
+ view.widgets['translations_usage'].vocabulary)
+ else:
+ self.assertIn(
+ ServiceUsage.LAUNCHPAD,
+ view.widgets['translations_usage'].vocabulary)
+
+ @staticmethod
+ def getViewContent(view):
+ with person_logged_in(view.request.principal):
+ return view()
+
+ @staticmethod
+ def hasLink(url):
+ return HTMLContains(Tag('link', 'a', attrs={'href': url}))
+
+ @classmethod
+ def getTranslationsContent(cls, product):
+ view = create_initialized_view(product, '+translations',
+ layer=TranslationsLayer,
+ principal=product.owner)
+ return cls.getViewContent(view)
+
+ def test_no_sync_links_for_proprietary(self):
+ product = self.factory.makeProduct()
+ content = self.getTranslationsContent(product)
+ series_url = canonical_url(
+ product.development_focus, view_name='+translations',
+ rootsite='translations')
+ manual_url = canonical_url(
+ product.development_focus, view_name='+translations-upload',
+ rootsite='translations')
+ automatic_url = canonical_url(
+ product.development_focus, view_name='+translations-settings',
+ rootsite='translations')
+ self.assertThat(content, self.hasLink(series_url))
+ self.assertThat(content, self.hasLink(manual_url))
+ self.assertThat(content, self.hasLink(automatic_url))
+ with person_logged_in(product.owner):
+ product.information_type = InformationType.PROPRIETARY
+ content = self.getTranslationsContent(product)
+ self.assertThat(content, Not(self.hasLink(series_url)))
+ self.assertThat(content, Not(self.hasLink(manual_url)))
+ self.assertThat(content, Not(self.hasLink(automatic_url)))
=== added file 'lib/lp/translations/browser/tests/test_productseries.py'
--- lib/lp/translations/browser/tests/test_productseries.py 1970-01-01 00:00:00 +0000
+++ lib/lp/translations/browser/tests/test_productseries.py 2012-11-30 20:20:39 +0000
@@ -0,0 +1,39 @@
+# Copyright 2012 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+__metaclass__ = type
+
+
+from soupmatchers import (
+ HTMLContains,
+ Tag,
+)
+from testtools.matchers import Not
+
+from lp.app.enums import InformationType
+from lp.testing import BrowserTestCase
+from lp.testing.layers import DatabaseFunctionalLayer
+
+
+class TestProductSeries(BrowserTestCase):
+
+ layer = DatabaseFunctionalLayer
+
+ @staticmethod
+ def hasAutoImport(value):
+ tag = Tag('importall', 'input',
+ attrs={'name': 'field.translations_autoimport_mode',
+ 'value': value})
+ return HTMLContains(tag)
+
+ def test_private_disables_imports(self):
+ owner = self.factory.makePerson()
+ product = self.factory.makeProduct(
+ owner=owner, information_type=InformationType.PROPRIETARY)
+ series = self.factory.makeProductSeries(product=product)
+ browser = self.getViewBrowser(series, '+translations-settings',
+ user=owner, rootsite='translations')
+ self.assertThat(browser.contents,
+ Not(self.hasAutoImport('IMPORT_TRANSLATIONS')))
+ self.assertThat(browser.contents,
+ Not(self.hasAutoImport('IMPORT_TEMPLATES')))
=== modified file 'lib/lp/translations/templates/product-portlet-translatables.pt'
--- lib/lp/translations/templates/product-portlet-translatables.pt 2012-07-06 06:02:33 +0000
+++ lib/lp/translations/templates/product-portlet-translatables.pt 2012-11-30 20:20:39 +0000
@@ -18,7 +18,7 @@
</ul>
</div>
-<div tal:condition="context/required:launchpad.Edit">
+<div tal:condition="view/allow_series_translation">
<div class="portlet" id="portlet-untranslatable-branches"
tal:condition="view/untranslatable_series">
<h3>Set up translations for a series</h3>
=== modified file 'lib/lp/translations/templates/productseries-translations-settings.pt'
--- lib/lp/translations/templates/productseries-translations-settings.pt 2012-11-08 03:55:11 +0000
+++ lib/lp/translations/templates/productseries-translations-settings.pt 2012-11-30 20:20:39 +0000
@@ -24,7 +24,7 @@
context/product/@@+portlet-not-using-launchpad"/>
</div>
- <div class="yui-g">
+ <div class="yui-g" tal:condition="not: context/product/private">
<div class="yui-u first portlet">
<div metal:use-macro="context/@@launchpad_form/form" class="portlet">
<div metal:fill-slot="extra_info">
Follow ups