← Back to team overview

launchpad-reviewers team mailing list archive

[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