Merge lp:~abentley/launchpad/no-private-translations into lp:launchpad

Proposed by Aaron Bentley
Status: Merged
Merged at revision: 16329
Proposed branch: lp:~abentley/launchpad/no-private-translations
Merge into: lp:launchpad
Diff against target: 479 lines (+237/-42)
10 files modified
lib/lp/registry/browser/product.py (+2/-1)
lib/lp/registry/model/product.py (+23/-5)
lib/lp/registry/model/productseries.py (+14/-1)
lib/lp/registry/tests/test_product.py (+57/-30)
lib/lp/registry/tests/test_productseries.py (+22/-2)
lib/lp/translations/browser/product.py (+5/-0)
lib/lp/translations/browser/tests/test_product_view.py (+72/-1)
lib/lp/translations/browser/tests/test_productseries.py (+40/-0)
lib/lp/translations/templates/product-portlet-translatables.pt (+1/-1)
lib/lp/translations/templates/productseries-translations-settings.pt (+1/-1)
To merge this branch: bzr merge lp:~abentley/launchpad/no-private-translations
Reviewer Review Type Date Requested Status
Brad Crittenden (community) Approve
Review via email: mp+137316@code.launchpad.net

Commit message

Prevent translations for private products.

Description of the change

= 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

To post a comment you must log in.
Revision history for this message
Brad Crittenden (bac) wrote :

Hi Aaron,

On IRC we discussed the term 'private' vs. 'proprietary' in user-visible messages. I think you agreed they should all be 'proprietary'.

Otherwise it looks good.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/registry/browser/product.py'
2--- lib/lp/registry/browser/product.py 2012-11-27 15:04:39 +0000
3+++ lib/lp/registry/browser/product.py 2012-11-30 20:54:23 +0000
4@@ -1332,7 +1332,8 @@
5 field.description = (
6 field.description.replace('pillar', 'project'))
7 usage_field.field = field
8- if (self.usage_fieldname == 'answers_usage' and
9+ if (self.usage_fieldname in
10+ ('answers_usage', 'translations_usage') and
11 self.context.information_type in
12 PROPRIETARY_INFORMATION_TYPES):
13 values = usage_field.field.vocabulary.items
14
15=== modified file 'lib/lp/registry/model/product.py'
16--- lib/lp/registry/model/product.py 2012-11-29 19:29:15 +0000
17+++ lib/lp/registry/model/product.py 2012-11-30 20:54:23 +0000
18@@ -212,6 +212,8 @@
19 from lp.translations.interfaces.customlanguagecode import (
20 IHasCustomLanguageCodes,
21 )
22+from lp.translations.interfaces.translations import (
23+ TranslationsBranchImportMode)
24 from lp.translations.model.customlanguagecode import (
25 CustomLanguageCode,
26 HasCustomLanguageCodesMixin,
27@@ -507,6 +509,17 @@
28 POTemplate.productseries == ProductSeries.id)
29 if not templates.is_empty():
30 yield CannotChangeInformationType('This project has translations.')
31+ if not self.getTranslationImportQueueEntries().is_empty():
32+ yield CannotChangeInformationType(
33+ 'This project has queued translations.')
34+ import_productseries = store.find(
35+ ProductSeries,
36+ ProductSeries.product == self.id,
37+ ProductSeries.translations_autoimport_mode !=
38+ TranslationsBranchImportMode.NO_IMPORT)
39+ if not import_productseries.is_empty():
40+ yield CannotChangeInformationType(
41+ 'Some product series have translation imports enabled.')
42 if not self.packagings.is_empty():
43 yield CannotChangeInformationType('Some series are packaged.')
44 if self.translations_usage == ServiceUsage.LAUNCHPAD:
45@@ -592,10 +605,18 @@
46 dbName="blueprints_usage", notNull=True,
47 schema=ServiceUsage,
48 default=ServiceUsage.UNKNOWN)
49+
50+ def validate_translations_usage(self, attr, value):
51+ if value == ServiceUsage.LAUNCHPAD and self.private:
52+ raise ProprietaryProduct(
53+ "Translations are not supported for proprietary products.")
54+ return value
55+
56 translations_usage = EnumCol(
57 dbName="translations_usage", notNull=True,
58 schema=ServiceUsage,
59- default=ServiceUsage.UNKNOWN)
60+ default=ServiceUsage.UNKNOWN,
61+ storm_validator=validate_translations_usage)
62
63 @property
64 def codehosting_usage(self):
65@@ -2072,16 +2093,13 @@
66
67 def getTranslatables(self):
68 """See `IProductSet`"""
69- user = getUtility(ILaunchBag).user
70- privacy_clause = self.getProductPrivacyFilter(user)
71 results = IStore(Product).find(
72 (Product, Person),
73 Product.active == True,
74 Product.id == ProductSeries.productID,
75 POTemplate.productseriesID == ProductSeries.id,
76 Product.translations_usage == ServiceUsage.LAUNCHPAD,
77- Person.id == Product._ownerID,
78- privacy_clause).config(
79+ Person.id == Product._ownerID).config(
80 distinct=True).order_by(Product.title)
81
82 # We only want Product - the other tables are just to populate
83
84=== modified file 'lib/lp/registry/model/productseries.py'
85--- lib/lp/registry/model/productseries.py 2012-10-18 14:05:41 +0000
86+++ lib/lp/registry/model/productseries.py 2012-11-30 20:54:23 +0000
87@@ -58,6 +58,7 @@
88 from lp.bugs.model.structuralsubscription import (
89 StructuralSubscriptionTargetMixin,
90 )
91+from lp.registry.errors import ProprietaryProduct
92 from lp.registry.interfaces.packaging import PackagingType
93 from lp.registry.interfaces.person import validate_person
94 from lp.registry.interfaces.productrelease import IProductReleaseSet
95@@ -147,11 +148,23 @@
96 notNull=False, default=None)
97 branch = ForeignKey(foreignKey='Branch', dbName='branch',
98 default=None)
99+
100+ def validate_autoimport_mode(self, attr, value):
101+ # Perform the normal validation for None
102+ if value is None:
103+ return value
104+ if (self.product.private and
105+ value != TranslationsBranchImportMode.NO_IMPORT):
106+ raise ProprietaryProduct('Translations are disabled for'
107+ ' proprietary projects.')
108+ return value
109+
110 translations_autoimport_mode = EnumCol(
111 dbName='translations_autoimport_mode',
112 notNull=True,
113 schema=TranslationsBranchImportMode,
114- default=TranslationsBranchImportMode.NO_IMPORT)
115+ default=TranslationsBranchImportMode.NO_IMPORT,
116+ storm_validator=validate_autoimport_mode)
117 translations_branch = ForeignKey(
118 dbName='translations_branch', foreignKey='Branch', notNull=False,
119 default=None)
120
121=== modified file 'lib/lp/registry/tests/test_product.py'
122--- lib/lp/registry/tests/test_product.py 2012-11-26 21:09:20 +0000
123+++ lib/lp/registry/tests/test_product.py 2012-11-30 20:54:23 +0000
124@@ -94,7 +94,6 @@
125 from lp.services.features.testing import FeatureFixture
126 from lp.services.webapp.authorization import check_permission
127 from lp.testing import (
128- ANONYMOUS,
129 celebrity_logged_in,
130 login,
131 person_logged_in,
132@@ -122,12 +121,14 @@
133 from lp.translations.interfaces.customlanguagecode import (
134 IHasCustomLanguageCodes,
135 )
136+from lp.translations.interfaces.translations import (
137+ TranslationsBranchImportMode)
138
139
140 class TestProduct(TestCaseWithFactory):
141 """Tests product object."""
142
143- layer = DatabaseFunctionalLayer
144+ layer = LaunchpadFunctionalLayer
145
146 def test_pillar_category(self):
147 # Products are really called Projects
148@@ -440,6 +441,7 @@
149 licenses=[License.OTHER_PROPRIETARY])
150 with person_logged_in(product.owner):
151 for usage in ServiceUsage:
152+ product.information_type = InformationType.PUBLIC
153 product.translations_usage = usage.value
154 for info_type in PROPRIETARY_INFORMATION_TYPES:
155 if product.translations_usage == ServiceUsage.LAUNCHPAD:
156@@ -548,6 +550,59 @@
157 'This project has translations.'):
158 raise error
159
160+ def test_checkInformationType_queued_translations(self):
161+ # Proprietary products must not have queued translations
162+ productseries = self.factory.makeProductSeries()
163+ product = productseries.product
164+ entry = self.factory.makeTranslationImportQueueEntry(
165+ productseries=productseries)
166+ for info_type in PROPRIETARY_INFORMATION_TYPES:
167+ with person_logged_in(product.owner):
168+ error, = list(product.checkInformationType(info_type))
169+ with ExpectedException(CannotChangeInformationType,
170+ 'This project has queued translations.'):
171+ raise error
172+ removeSecurityProxy(entry).delete(entry.id)
173+ with person_logged_in(product.owner):
174+ for info_type in PROPRIETARY_INFORMATION_TYPES:
175+ self.assertContentEqual(
176+ [], product.checkInformationType(info_type))
177+
178+ def test_checkInformationType_auto_translation_imports(self):
179+ # Proprietary products must not be at risk of creating translations.
180+ productseries = self.factory.makeProductSeries()
181+ product = productseries.product
182+ self.useContext(person_logged_in(product.owner))
183+ for mode in TranslationsBranchImportMode.items:
184+ if mode == TranslationsBranchImportMode.NO_IMPORT:
185+ continue
186+ productseries.translations_autoimport_mode = mode
187+ for info_type in PROPRIETARY_INFORMATION_TYPES:
188+ error, = list(product.checkInformationType(info_type))
189+ with ExpectedException(CannotChangeInformationType,
190+ 'Some product series have translation imports enabled.'):
191+ raise error
192+ productseries.translations_autoimport_mode = (
193+ TranslationsBranchImportMode.NO_IMPORT)
194+ for info_type in PROPRIETARY_INFORMATION_TYPES:
195+ self.assertContentEqual(
196+ [], product.checkInformationType(info_type))
197+
198+ def test_private_forbids_translations(self):
199+ owner = self.factory.makePerson()
200+ product = self.factory.makeProduct(owner=owner)
201+ self.useContext(person_logged_in(owner))
202+ for info_type in PROPRIETARY_INFORMATION_TYPES:
203+ product.information_type = info_type
204+ with ExpectedException(
205+ ProprietaryProduct,
206+ "Translations are not supported for proprietary products."):
207+ product.translations_usage = ServiceUsage.LAUNCHPAD
208+ for usage in ServiceUsage.items:
209+ if usage == ServiceUsage.LAUNCHPAD:
210+ continue
211+ product.translations_usage = usage
212+
213 def createProduct(self, information_type=None, license=None):
214 # convenience method for testing IProductSet.createProduct rather than
215 # self.factory.makeProduct
216@@ -2248,34 +2303,6 @@
217 self.assertIn(embargoed, result)
218 self.assertIn(proprietary, result)
219
220- def test_getTranslatables_filters_private_products(self):
221- # ProductSet.getTranslatables() returns private translatable
222- # products only for user that have grants for these products.
223- owner = self.factory.makePerson()
224- product = self.factory.makeProduct(
225- owner=owner, translations_usage=ServiceUsage.LAUNCHPAD,
226- information_type=InformationType.PROPRIETARY)
227- series = self.factory.makeProductSeries(product)
228- with person_logged_in(owner):
229- self.factory.makePOTemplate(productseries=series)
230- # Anonymous users do not see private products.
231- with person_logged_in(ANONYMOUS):
232- translatables = getUtility(IProductSet).getTranslatables()
233- self.assertNotIn(product, list(translatables))
234- # Ordinary users do not see private products.
235- user = self.factory.makePerson()
236- with person_logged_in(user):
237- translatables = getUtility(IProductSet).getTranslatables()
238- self.assertNotIn(product, list(translatables))
239- # Users with policy grants on private products see them.
240- with person_logged_in(owner):
241- getUtility(IService, 'sharing').sharePillarInformation(
242- product, user, owner,
243- {InformationType.PROPRIETARY: SharingPermission.ALL})
244- with person_logged_in(user):
245- translatables = getUtility(IProductSet).getTranslatables()
246- self.assertIn(product, list(translatables))
247-
248
249 class TestProductSetWebService(WebServiceTestCase):
250
251
252=== modified file 'lib/lp/registry/tests/test_productseries.py'
253--- lib/lp/registry/tests/test_productseries.py 2012-11-27 22:02:34 +0000
254+++ lib/lp/registry/tests/test_productseries.py 2012-11-30 20:54:23 +0000
255@@ -16,11 +16,17 @@
256 from zope.security.interfaces import Unauthorized
257 from zope.security.proxy import removeSecurityProxy
258
259-from lp.app.enums import InformationType
260+from lp.app.enums import (
261+ InformationType,
262+ PROPRIETARY_INFORMATION_TYPES,
263+ )
264 from lp.app.interfaces.informationtype import IInformationType
265 from lp.app.interfaces.services import IService
266 from lp.registry.enums import SharingPermission
267-from lp.registry.errors import CannotPackageProprietaryProduct
268+from lp.registry.errors import (
269+ CannotPackageProprietaryProduct,
270+ ProprietaryProduct,
271+ )
272 from lp.registry.interfaces.distribution import IDistributionSet
273 from lp.registry.interfaces.distroseries import IDistroSeriesSet
274 from lp.registry.interfaces.productseries import (
275@@ -64,6 +70,20 @@
276 self.assertEqual(
277 IInformationType(series).information_type, information_type)
278
279+ def test_private_forbids_autoimport(self):
280+ # Autoimports are forbidden if products are proprietary/embargoed.
281+ series = self.factory.makeProductSeries()
282+ self.useContext(person_logged_in(series.product.owner))
283+ for info_type in PROPRIETARY_INFORMATION_TYPES:
284+ series.product.information_type = info_type
285+ for mode in TranslationsBranchImportMode.items:
286+ if mode == TranslationsBranchImportMode.NO_IMPORT:
287+ continue
288+ with ExpectedException(ProprietaryProduct,
289+ 'Translations are disabled for proprietary'
290+ ' projects.'):
291+ series.translations_autoimport_mode = mode
292+
293
294 class ProductSeriesReleasesTestCase(TestCaseWithFactory):
295 """Test for ProductSeries.release property."""
296
297=== modified file 'lib/lp/translations/browser/product.py'
298--- lib/lp/translations/browser/product.py 2012-01-01 02:58:52 +0000
299+++ lib/lp/translations/browser/product.py 2012-11-30 20:54:23 +0000
300@@ -136,3 +136,8 @@
301 return [series for series in self.context.series if (
302 series.status != SeriesStatus.OBSOLETE and
303 series not in translatable)]
304+
305+ @property
306+ def allow_series_translation(self):
307+ return (check_permission("launchpad.Edit", self.context) and not
308+ self.context.private)
309
310=== modified file 'lib/lp/translations/browser/tests/test_product_view.py'
311--- lib/lp/translations/browser/tests/test_product_view.py 2012-01-01 02:58:52 +0000
312+++ lib/lp/translations/browser/tests/test_product_view.py 2012-11-30 20:54:23 +0000
313@@ -3,12 +3,25 @@
314
315 __metaclass__ = type
316
317-from lp.app.enums import ServiceUsage
318+
319+from soupmatchers import (
320+ HTMLContains,
321+ Tag,
322+)
323+from testtools.matchers import Not
324+
325+from lp.app.enums import (
326+ InformationType,
327+ PUBLIC_PROPRIETARY_INFORMATION_TYPES,
328+ ServiceUsage,
329+ )
330 from lp.registry.interfaces.series import SeriesStatus
331+from lp.services.webapp import canonical_url
332 from lp.services.webapp.servers import LaunchpadTestRequest
333 from lp.testing import (
334 celebrity_logged_in,
335 login_person,
336+ person_logged_in,
337 TestCaseWithFactory,
338 )
339 from lp.testing.layers import (
340@@ -16,6 +29,7 @@
341 LaunchpadZopelessLayer,
342 )
343 from lp.testing.views import create_view
344+from lp.testing.views import create_initialized_view
345 from lp.translations.browser.product import ProductView
346 from lp.translations.publisher import TranslationsLayer
347
348@@ -114,3 +128,60 @@
349 view = create_view(product, '+translations',
350 layer=TranslationsLayer)
351 self.assertEqual(True, view.can_configure_translations())
352+
353+ def test_launchpad_not_listed_for_proprietary(self):
354+ product = self.factory.makeProduct()
355+ with person_logged_in(product.owner):
356+ for info_type in PUBLIC_PROPRIETARY_INFORMATION_TYPES:
357+ product.information_type = info_type
358+ view = create_initialized_view(
359+ product, '+configure-translations',
360+ layer=TranslationsLayer)
361+ if product.private:
362+ self.assertNotIn(
363+ ServiceUsage.LAUNCHPAD,
364+ view.widgets['translations_usage'].vocabulary)
365+ else:
366+ self.assertIn(
367+ ServiceUsage.LAUNCHPAD,
368+ view.widgets['translations_usage'].vocabulary)
369+
370+ @staticmethod
371+ def getViewContent(view):
372+ with person_logged_in(view.request.principal):
373+ return view()
374+
375+ @staticmethod
376+ def hasLink(url):
377+ return HTMLContains(Tag('link', 'a', attrs={'href': url}))
378+
379+ @classmethod
380+ def getTranslationsContent(cls, product):
381+ view = create_initialized_view(product, '+translations',
382+ layer=TranslationsLayer,
383+ principal=product.owner)
384+ return cls.getViewContent(view)
385+
386+ def test_no_sync_links_for_proprietary(self):
387+ # Proprietary products don't have links for synchronizing
388+ # productseries.
389+ product = self.factory.makeProduct()
390+ content = self.getTranslationsContent(product)
391+ series_url = canonical_url(
392+ product.development_focus, view_name='+translations',
393+ rootsite='translations')
394+ manual_url = canonical_url(
395+ product.development_focus, view_name='+translations-upload',
396+ rootsite='translations')
397+ automatic_url = canonical_url(
398+ product.development_focus, view_name='+translations-settings',
399+ rootsite='translations')
400+ self.assertThat(content, self.hasLink(series_url))
401+ self.assertThat(content, self.hasLink(manual_url))
402+ self.assertThat(content, self.hasLink(automatic_url))
403+ with person_logged_in(product.owner):
404+ product.information_type = InformationType.PROPRIETARY
405+ content = self.getTranslationsContent(product)
406+ self.assertThat(content, Not(self.hasLink(series_url)))
407+ self.assertThat(content, Not(self.hasLink(manual_url)))
408+ self.assertThat(content, Not(self.hasLink(automatic_url)))
409
410=== added file 'lib/lp/translations/browser/tests/test_productseries.py'
411--- lib/lp/translations/browser/tests/test_productseries.py 1970-01-01 00:00:00 +0000
412+++ lib/lp/translations/browser/tests/test_productseries.py 2012-11-30 20:54:23 +0000
413@@ -0,0 +1,40 @@
414+# Copyright 2012 Canonical Ltd. This software is licensed under the
415+# GNU Affero General Public License version 3 (see the file LICENSE).
416+
417+__metaclass__ = type
418+
419+
420+from soupmatchers import (
421+ HTMLContains,
422+ Tag,
423+)
424+from testtools.matchers import Not
425+
426+from lp.app.enums import InformationType
427+from lp.testing import BrowserTestCase
428+from lp.testing.layers import DatabaseFunctionalLayer
429+
430+
431+class TestProductSeries(BrowserTestCase):
432+
433+ layer = DatabaseFunctionalLayer
434+
435+ @staticmethod
436+ def hasAutoImport(value):
437+ tag = Tag('importall', 'input',
438+ attrs={'name': 'field.translations_autoimport_mode',
439+ 'value': value})
440+ return HTMLContains(tag)
441+
442+ def test_private_disables_imports(self):
443+ # Proprietary products disable import options.
444+ owner = self.factory.makePerson()
445+ product = self.factory.makeProduct(
446+ owner=owner, information_type=InformationType.PROPRIETARY)
447+ series = self.factory.makeProductSeries(product=product)
448+ browser = self.getViewBrowser(series, '+translations-settings',
449+ user=owner, rootsite='translations')
450+ self.assertThat(browser.contents,
451+ Not(self.hasAutoImport('IMPORT_TRANSLATIONS')))
452+ self.assertThat(browser.contents,
453+ Not(self.hasAutoImport('IMPORT_TEMPLATES')))
454
455=== modified file 'lib/lp/translations/templates/product-portlet-translatables.pt'
456--- lib/lp/translations/templates/product-portlet-translatables.pt 2012-07-06 06:02:33 +0000
457+++ lib/lp/translations/templates/product-portlet-translatables.pt 2012-11-30 20:54:23 +0000
458@@ -18,7 +18,7 @@
459 </ul>
460 </div>
461
462-<div tal:condition="context/required:launchpad.Edit">
463+<div tal:condition="view/allow_series_translation">
464 <div class="portlet" id="portlet-untranslatable-branches"
465 tal:condition="view/untranslatable_series">
466 <h3>Set up translations for a series</h3>
467
468=== modified file 'lib/lp/translations/templates/productseries-translations-settings.pt'
469--- lib/lp/translations/templates/productseries-translations-settings.pt 2012-11-08 03:55:11 +0000
470+++ lib/lp/translations/templates/productseries-translations-settings.pt 2012-11-30 20:54:23 +0000
471@@ -24,7 +24,7 @@
472 context/product/@@+portlet-not-using-launchpad"/>
473 </div>
474
475- <div class="yui-g">
476+ <div class="yui-g" tal:condition="not: context/product/private">
477 <div class="yui-u first portlet">
478 <div metal:use-macro="context/@@launchpad_form/form" class="portlet">
479 <div metal:fill-slot="extra_info">