Merge lp:~jtv/launchpad/recife-policy-mixin into lp:~launchpad/launchpad/recife

Proposed by Jeroen T. Vermeulen on 2010-11-08
Status: Merged
Merged at revision: 9179
Proposed branch: lp:~jtv/launchpad/recife-policy-mixin
Merge into: lp:~launchpad/launchpad/recife
Diff against target: 688 lines (+408/-78)
11 files modified
lib/lp/registry/interfaces/product.py (+1/-5)
lib/lp/registry/model/distribution.py (+2/-1)
lib/lp/registry/model/product.py (+14/-21)
lib/lp/registry/model/projectgroup.py (+3/-2)
lib/lp/translations/browser/pofile.py (+12/-17)
lib/lp/translations/interfaces/potemplate.py (+6/-0)
lib/lp/translations/interfaces/translationpolicy.py (+53/-2)
lib/lp/translations/model/potemplate.py (+9/-24)
lib/lp/translations/model/translationgroup.py (+20/-6)
lib/lp/translations/model/translationpolicy.py (+104/-0)
lib/lp/translations/tests/test_translationpolicy.py (+184/-0)
To merge this branch: bzr merge lp:~jtv/launchpad/recife-policy-mixin
Reviewer Review Type Date Requested Status
Brad Crittenden (community) 2010-11-08 Approve on 2010-11-08
Review via email: mp+40300@code.launchpad.net

Commit Message

TranslationPolicyMixin

Description of the Change

= TranslationPolicy mixin =

In my ongoing work for the Recife feature branch, the ITranslationPolicy is growing from a placeholder for two pieces of configuration data into a place where translation policy is derived from that configuration data. Product, ProjectGroup, and Distribution all implement the interface.

At this point I'm extending the interface further and introducing TranslationPolicyMixin, a base class that implements most of ITranslationPolicy in one place. The branch is meant for merging into the feature branch, not devel or db-devel.

I'm not actually making much use of the new class yet, but I have prototypes that move the related decision-making into the TranslationPolicyMixin. I'm splitting the actual landing up into multiple smaller branches. (That's also why I'm using the multi-line import format in a case that would have fit into a single line: I'm about to add to that import anyway). For now you'll see some less significant code being reworked to use the policy methods.

For testing I just ran all Translations tests except the windmill ones (which crash firefox on my system):
{{{
./bin/test -vvc lp.translations.tests
}}}

Needless to say, these all pass. There were one or two tiny pieces of lint left, but none that I caused or dare fix.

Jeroen

To post a comment you must log in.
Robert Collins (lifeless) wrote :

I think this would be better as a dedicated class not a mixin. Other than that it seems reasonable to me.

Jeroen T. Vermeulen (jtv) wrote :

Okay: I'm reworking this to be a dedicated class.

Jeroen T. Vermeulen (jtv) wrote :

The rework gets too big. I'll have to leave that for a separate branch.

Brad Crittenden (bac) wrote :

Hi Jeroen,

This branch continuing your work looks good. I understand your desire to not do the conversion from a mixin in this branch,but as follow on work.

In getTranslationPolicy you test for productseries is not None to determine whether it is a product/projectgroup or distro. I'd like to see a test for distroseries before you use it and raise an AssertionError if both are None.

Please run the copyright and import sort tools on your branch b/c I didn't pay any attention to sorting, etc.

Other than that the branch looks good.

review: Approve
Jeroen T. Vermeulen (jtv) wrote :

Hi Brad. Thanks for the review!

The restrictions POTemplate are such that one of {productseries, distroseries} is always None and the other is not. An attempt to do it differently would result in a database error, or possibly an assertion failure even before it gets to that. The fields are initialized once and then never changed, so there are no reasonable transient states that might deviate from the rule either.

Will run copyright and import sort tools. Thanks for the reminder.

Jeroen

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/registry/interfaces/product.py'
2--- lib/lp/registry/interfaces/product.py 2010-11-05 09:59:37 +0000
3+++ lib/lp/registry/interfaces/product.py 2010-11-08 09:02:43 +0000
4@@ -1,4 +1,4 @@
5-# Copyright 2009 Canonical Ltd. This software is licensed under the
6+# Copyright 2009-2010 Canonical Ltd. This software is licensed under the
7 # GNU Affero General Public License version 3 (see the file LICENSE).
8
9 # pylint: disable-msg=E0211,E0213
10@@ -706,10 +706,6 @@
11 "groups for a product. There can be several: one from the product, "
12 "and potentially one from the project, too.")
13
14- aggregatetranslationpermission = Attribute("The translation permission "
15- "that applies to translations in this product, based on the "
16- "permissions that apply to the product as well as its project.")
17-
18 commercial_subscription = exported(
19 Reference(
20 ICommercialSubscription,
21
22=== modified file 'lib/lp/registry/model/distribution.py'
23--- lib/lp/registry/model/distribution.py 2010-11-02 20:10:56 +0000
24+++ lib/lp/registry/model/distribution.py 2010-11-08 09:02:43 +0000
25@@ -192,6 +192,7 @@
26 from lp.translations.model.hastranslationimports import (
27 HasTranslationImportsMixin,
28 )
29+from lp.translations.model.translationpolicy import TranslationPolicyMixin
30
31
32 class Distribution(SQLBase, BugTargetBase, MakesAnnouncements,
33@@ -199,7 +200,7 @@
34 HasTranslationImportsMixin, KarmaContextMixin,
35 OfficialBugTagTargetMixin, QuestionTargetMixin,
36 StructuralSubscriptionTargetMixin, HasMilestonesMixin,
37- HasBugHeatMixin):
38+ HasBugHeatMixin, TranslationPolicyMixin):
39 """A distribution of an operating system, e.g. Debian GNU/Linux."""
40 implements(
41 IDistribution, IFAQTarget, IHasBugHeat, IHasBugSupervisor,
42
43=== modified file 'lib/lp/registry/model/product.py'
44--- lib/lp/registry/model/product.py 2010-11-03 22:37:55 +0000
45+++ lib/lp/registry/model/product.py 2010-11-08 09:02:43 +0000
46@@ -174,6 +174,7 @@
47 HasTranslationImportsMixin,
48 )
49 from lp.translations.model.potemplate import POTemplate
50+from lp.translations.model.translationpolicy import TranslationPolicyMixin
51
52
53 def get_license_status(license_approved, license_reviewed, licenses):
54@@ -290,7 +291,7 @@
55 HasAliasMixin, StructuralSubscriptionTargetMixin,
56 HasMilestonesMixin, OfficialBugTagTargetMixin, HasBranchesMixin,
57 HasCustomLanguageCodesMixin, HasMergeProposalsMixin,
58- HasBugHeatMixin, HasCodeImportsMixin):
59+ HasBugHeatMixin, HasCodeImportsMixin, TranslationPolicyMixin):
60 """A Product."""
61
62 implements(
63@@ -1045,26 +1046,18 @@
64
65 @property
66 def translationgroups(self):
67- tg = []
68- if self.translationgroup:
69- tg.append(self.translationgroup)
70- if self.project:
71- if self.project.translationgroup:
72- if self.project.translationgroup not in tg:
73- tg.append(self.project.translationgroup)
74-
75- @property
76- def aggregatetranslationpermission(self):
77- perms = [self.translationpermission]
78- if self.project:
79- perms.append(self.project.translationpermission)
80- # XXX Carlos Perello Marin 2005-06-02:
81- # Reviewer please describe a better way to explicitly order
82- # the enums. The spec describes the order, and the values make
83- # it work, and there is space left for new values so we can
84- # ensure a consistent sort order in future, but there should be
85- # a better way.
86- return max(perms)
87+ return reversed(self.getTranslationGroups())
88+
89+ def isTranslationsOwner(self, person):
90+ """See `ITranslationPolicy`."""
91+ # A Product owner gets special translation privileges.
92+ return person.inTeam(self.owner)
93+
94+ def getInheritedTranslationPolicy(self):
95+ """See `ITranslationPolicy`."""
96+ # A Product inherits parts of it its effective translation
97+ # policy from its ProjectGroup, if any.
98+ return self.project
99
100 @property
101 def has_any_specifications(self):
102
103=== modified file 'lib/lp/registry/model/projectgroup.py'
104--- lib/lp/registry/model/projectgroup.py 2010-11-02 20:10:56 +0000
105+++ lib/lp/registry/model/projectgroup.py 2010-11-08 09:02:43 +0000
106@@ -1,4 +1,4 @@
107-# Copyright 2009 Canonical Ltd. This software is licensed under the
108+# Copyright 2009-2010 Canonical Ltd. This software is licensed under the
109 # GNU Affero General Public License version 3 (see the file LICENSE).
110
111 # pylint: disable-msg=E0611,W0212
112@@ -105,6 +105,7 @@
113 )
114 from lp.services.worlddata.model.language import Language
115 from lp.translations.interfaces.translationgroup import TranslationPermission
116+from lp.translations.model.translationpolicy import TranslationPolicyMixin
117
118
119 class ProjectGroup(SQLBase, BugTargetBase, HasSpecificationsMixin,
120@@ -112,7 +113,7 @@
121 KarmaContextMixin, BranchVisibilityPolicyMixin,
122 StructuralSubscriptionTargetMixin,
123 HasBranchesMixin, HasMergeProposalsMixin, HasBugHeatMixin,
124- HasMilestonesMixin):
125+ HasMilestonesMixin, TranslationPolicyMixin):
126 """A ProjectGroup"""
127
128 implements(IProjectGroup, IFAQCollection, IHasBugHeat, IHasIcon, IHasLogo,
129
130=== modified file 'lib/lp/translations/browser/pofile.py'
131--- lib/lp/translations/browser/pofile.py 2010-11-04 09:32:28 +0000
132+++ lib/lp/translations/browser/pofile.py 2010-11-08 09:02:43 +0000
133@@ -503,24 +503,19 @@
134 Duplicates are eliminated; every translation group will occur
135 at most once.
136 """
137- language = self.context.language
138 managers = []
139- groups = set()
140- for group in self.context.potemplate.translationgroups:
141- if group not in groups:
142- translator = group.query_translator(language)
143- if translator is None:
144- team = None
145- style_guide_url = None
146- else:
147- team = translator.translator
148- style_guide_url = translator.style_guide_url
149- managers.append({
150- 'group': group,
151- 'team': team,
152- 'style_guide_url': style_guide_url,
153- })
154- groups.add(group)
155+ policy = self.context.potemplate.getTranslationPolicy()
156+ translators = policy.getTranslators(self.context.language)
157+ for group, translator, team in reversed(translators):
158+ if translator is None:
159+ style_guide_url = None
160+ else:
161+ style_guide_url = translator.style_guide_url
162+ managers.append({
163+ 'group': group,
164+ 'team': team,
165+ 'style_guide_url': style_guide_url,
166+ })
167 return managers
168
169
170
171=== modified file 'lib/lp/translations/interfaces/potemplate.py'
172--- lib/lp/translations/interfaces/potemplate.py 2010-10-18 16:36:46 +0000
173+++ lib/lp/translations/interfaces/potemplate.py 2010-11-08 09:02:43 +0000
174@@ -517,6 +517,12 @@
175 def awardKarma(person, action_name):
176 """Award karma for a translation action on this template."""
177
178+ def getTranslationPolicy():
179+ """Return the applicable `ITranslationPolicy` object.
180+
181+ The returned object is either a `Product` or a `Distribution`.
182+ """
183+
184
185 class IPOTemplateSubset(Interface):
186 """A subset of POTemplate."""
187
188=== modified file 'lib/lp/translations/interfaces/translationpolicy.py'
189--- lib/lp/translations/interfaces/translationpolicy.py 2010-11-05 08:16:14 +0000
190+++ lib/lp/translations/interfaces/translationpolicy.py 2010-11-08 09:02:43 +0000
191@@ -1,7 +1,7 @@
192 # Copyright 2010 Canonical Ltd. This software is licensed under the
193 # GNU Affero General Public License version 3 (see the file LICENSE).
194
195-"""Translation permissions policy."""
196+"""Translation access and sharing policy."""
197
198 __metaclass__ = type
199 __all__ = [
200@@ -9,13 +9,23 @@
201 ]
202
203 from zope.interface import Interface
204-from zope.schema import Choice
205+from zope.schema import (
206+ Choice,
207+ )
208
209 from canonical.launchpad import _
210 from lp.translations.interfaces.translationgroup import TranslationPermission
211
212
213 class ITranslationPolicy(Interface):
214+ """Permissions and sharing policy for translatable pillars.
215+
216+ A translation policy defines who can edit translations, and who can
217+ add suggestions. (The ability to edit also implies the ability to
218+ enter suggestions). Everyone else is allowed only to view the
219+ translations.
220+ """
221+
222 translationgroup = Choice(
223 title = _("Translation group"),
224 description = _("The translation group that helps review "
225@@ -30,3 +40,44 @@
226 " balance openness and control for their translations."),
227 required=True,
228 vocabulary=TranslationPermission)
229+
230+ def getTranslationGroups(self):
231+ """List all applicable translation groups.
232+
233+ This may be an empty list, or a list containing just this
234+ policy's translation group, or for a product that is part of a
235+ project group, possibly a list of two translation groups.
236+
237+ If there is an inherited policy, its translation group comes
238+ first. Duplicates are removed.
239+ """
240+
241+ def getTranslators(language, store=None):
242+ """Find the applicable `TranslationGroup`(s) and translators.
243+
244+ Zero, one, or two translation groups may apply. Each may have a
245+ `Translator` for the language, with either a person or a team
246+ assigned.
247+
248+ In the case of a product in a project group, there may be up to
249+ two entries. In that case, the entry from the project group
250+ comes first.
251+
252+ :param language: The language that you want the translators for.
253+ :type language: ILanguage
254+ :param store: Optionally a specific store to retrieve from.
255+ :type store: Store
256+ :return: A result set of zero or more tuples:
257+ (`TranslationGroup`, `Translator`, `Person`). The
258+ translation group is always present and unique. The person
259+ is present if and only if the translator is present. The
260+ translator is unique if present, but the person need not be.
261+ """
262+
263+ def getEffectiveTranslationPermission(self):
264+ """Get the effective `TranslationPermission`.
265+
266+ Returns the strictest applicable permission out of
267+ `self.translationpermission` and any inherited
268+ `TranslationPermission`.
269+ """
270
271=== modified file 'lib/lp/translations/model/potemplate.py'
272--- lib/lp/translations/model/potemplate.py 2010-11-05 08:11:53 +0000
273+++ lib/lp/translations/model/potemplate.py 2010-11-08 09:02:43 +0000
274@@ -316,37 +316,22 @@
275 else:
276 return None
277
278+ def getTranslationPolicy(self):
279+ """See `IPOTemplate`."""
280+ if self.productseries is not None:
281+ return self.productseries.product
282+ else:
283+ return self.distroseries.distribution
284+
285 @property
286 def translationgroups(self):
287 """See `IPOTemplate`."""
288- ret = []
289- if self.distroseries:
290- tg = self.distroseries.distribution.translationgroup
291- if tg is not None:
292- ret.append(tg)
293- elif self.productseries:
294- product_tg = self.productseries.product.translationgroup
295- if product_tg is not None:
296- ret.append(product_tg)
297- project = self.productseries.product.project
298- if project is not None:
299- if project.translationgroup is not None:
300- ret.append(project.translationgroup)
301- else:
302- raise NotImplementedError('Cannot find translation groups.')
303- return ret
304+ return self.getTranslationPolicy().getTranslationGroups()
305
306 @property
307 def translationpermission(self):
308 """See `IPOTemplate`."""
309- if self.distroseries:
310- # in the case of a distro template, use the distro translation
311- # permission settings
312- return self.distroseries.distribution.translationpermission
313- elif self.productseries:
314- # for products, use the "most restrictive permission" between
315- # project and product.
316- return self.productseries.product.aggregatetranslationpermission
317+ return self.getTranslationPolicy().getEffectiveTranslationPermission()
318
319 @property
320 def relatives_by_name(self):
321
322=== modified file 'lib/lp/translations/model/translationgroup.py'
323--- lib/lp/translations/model/translationgroup.py 2010-09-03 10:59:24 +0000
324+++ lib/lp/translations/model/translationgroup.py 2010-11-08 09:02:43 +0000
325@@ -33,13 +33,7 @@
326 from canonical.launchpad.interfaces.lpstorm import ISlaveStore
327 from lp.app.errors import NotFoundError
328 from lp.registry.interfaces.person import validate_public_person
329-from lp.registry.model.distribution import Distribution
330 from lp.registry.model.person import Person
331-from lp.registry.model.product import (
332- Product,
333- ProductWithLicenses,
334- )
335-from lp.registry.model.projectgroup import ProjectGroup
336 from lp.registry.model.teammembership import TeamParticipation
337 from lp.services.database.prejoin import prejoin
338 from lp.services.worlddata.model.language import Language
339@@ -108,10 +102,18 @@
340
341 @property
342 def products(self):
343+ """See `ITranslationGroup`."""
344+ # Avoid circular imports.
345+ from lp.registry.model.product import Product
346+
347 return Product.selectBy(translationgroup=self.id, active=True)
348
349 @property
350 def projects(self):
351+ """See `ITranslationGroup`."""
352+ # Avoid circular imports.
353+ from lp.registry.model.projectgroup import ProjectGroup
354+
355 return ProjectGroup.selectBy(translationgroup=self.id, active=True)
356
357 # A limit of projects to get for the `top_projects`.
358@@ -178,6 +180,12 @@
359
360 def fetchProjectsForDisplay(self):
361 """See `ITranslationGroup`."""
362+ # Avoid circular imports.
363+ from lp.registry.model.product import (
364+ Product,
365+ ProductWithLicenses,
366+ )
367+
368 using = [
369 Product,
370 LeftJoin(LibraryFileAlias, LibraryFileAlias.id == Product.iconID),
371@@ -202,6 +210,9 @@
372
373 def fetchProjectGroupsForDisplay(self):
374 """See `ITranslationGroup`."""
375+ # Avoid circular imports.
376+ from lp.registry.model.projectgroup import ProjectGroup
377+
378 using = [
379 ProjectGroup,
380 LeftJoin(
381@@ -225,6 +236,9 @@
382
383 def fetchDistrosForDisplay(self):
384 """See `ITranslationGroup`."""
385+ # Avoid circular imports.
386+ from lp.registry.model.distribution import Distribution
387+
388 using = [
389 Distribution,
390 LeftJoin(
391
392=== added file 'lib/lp/translations/model/translationpolicy.py'
393--- lib/lp/translations/model/translationpolicy.py 1970-01-01 00:00:00 +0000
394+++ lib/lp/translations/model/translationpolicy.py 2010-11-08 09:02:43 +0000
395@@ -0,0 +1,104 @@
396+# Copyright 2010 Canonical Ltd. This software is licensed under the
397+# GNU Affero General Public License version 3 (see the file LICENSE).
398+
399+"""Translation access and sharing policy."""
400+
401+__metaclass__ = type
402+__all__ = [
403+ 'TranslationPolicyMixin',
404+ ]
405+
406+from storm.expr import (
407+ And,
408+ LeftJoin,
409+ )
410+from zope.component import getUtility
411+
412+from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
413+from canonical.launchpad.interfaces.lpstorm import IStore
414+from lp.registry.model.person import Person
415+from lp.translations.interfaces.translationsperson import ITranslationsPerson
416+from lp.translations.model.translationgroup import TranslationGroup
417+from lp.translations.model.translator import Translator
418+
419+
420+class TranslationPolicyMixin:
421+ """Implementation mixin for `ITranslationPolicy`."""
422+
423+ def getInheritedTranslationPolicy(self):
424+ """Get any `ITranslationPolicy` objects that this one inherits.
425+
426+ To be overridden by the implementing class. A `Product` may
427+ inherit a policy from a `ProjectGroup` it's in.
428+ """
429+ return None
430+
431+ def isTranslationsOwner(self, person):
432+ """Is `person` one of the owners of these translations?
433+
434+ To be overridden by the implementing class if it grants special
435+ translation rights to certain people.
436+ """
437+ return False
438+
439+ def _hasSpecialTranslationPrivileges(self, person):
440+ """Does this person have special translation editing rights here?"""
441+ celebs = getUtility(ILaunchpadCelebrities)
442+ return (
443+ person.inTeam(celebs.admin) or
444+ person.inTeam(celebs.rosetta_experts) or
445+ self.isTranslationsOwner(person))
446+
447+ def _canTranslate(self, person):
448+ """Is `person` in a position to translate?
449+
450+ Someone who has declined the translations relicensing agreement
451+ is not. Someone who hasn't decided on the agreement yet is, but
452+ may be redirected to a form to sign first.
453+ """
454+ translations_person = ITranslationsPerson(person)
455+ agreement = translations_person.translations_relicensing_agreement
456+ return agreement is not False
457+
458+ def getTranslationGroups(self):
459+ """See `ITranslationPolicy`."""
460+ inherited = self.getInheritedTranslationPolicy()
461+ if inherited is None:
462+ groups = []
463+ else:
464+ groups = inherited.getTranslationGroups()
465+ my_group = self.translationgroup
466+ if my_group is not None and my_group not in groups:
467+ groups.append(my_group)
468+ return groups
469+
470+ def _getTranslator(self, translationgroup, language, store):
471+ """Retrieve one (TranslationGroup, Translator, Person) tuple."""
472+ translator_join = LeftJoin(Translator, And(
473+ Translator.translationgroupID == TranslationGroup.id,
474+ Translator.languageID == language.id))
475+ person_join = LeftJoin(
476+ Person, Person.id == Translator.translatorID)
477+
478+ source = store.using(TranslationGroup, translator_join, person_join)
479+ return source.find(
480+ (TranslationGroup, Translator, Person),
481+ TranslationGroup.id == translationgroup.id).one()
482+
483+ def getTranslators(self, language, store=None):
484+ """See `ITranslationPolicy`."""
485+ if store is None:
486+ store = IStore(TranslationGroup)
487+ return [
488+ self._getTranslator(group, language, store)
489+ for group in self.getTranslationGroups()]
490+
491+ def getEffectiveTranslationPermission(self):
492+ """See `ITranslationPolicy`."""
493+ inherited = self.getInheritedTranslationPolicy()
494+ if inherited is None:
495+ return self.translationpermission
496+ else:
497+ return max([
498+ self.translationpermission,
499+ inherited.getEffectiveTranslationPermission()])
500
501=== added file 'lib/lp/translations/tests/test_translationpolicy.py'
502--- lib/lp/translations/tests/test_translationpolicy.py 1970-01-01 00:00:00 +0000
503+++ lib/lp/translations/tests/test_translationpolicy.py 2010-11-08 09:02:43 +0000
504@@ -0,0 +1,184 @@
505+# Copyright 2010 Canonical Ltd. This software is licensed under the
506+# GNU Affero General Public License version 3 (see the file LICENSE).
507+
508+"""Test `TranslationPolicyMixin`."""
509+
510+__metaclass__ = type
511+
512+from zope.component import getUtility
513+from zope.interface import implements
514+
515+from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
516+from canonical.testing.layers import ZopelessDatabaseLayer
517+from lp.testing import TestCaseWithFactory
518+from lp.testing.fakemethod import FakeMethod
519+from lp.translations.interfaces.translationgroup import TranslationPermission
520+from lp.translations.interfaces.translationpolicy import ITranslationPolicy
521+from lp.translations.interfaces.translationsperson import ITranslationsPerson
522+from lp.translations.interfaces.translator import ITranslatorSet
523+from lp.translations.model.translationpolicy import TranslationPolicyMixin
524+
525+
526+class TranslationPolicyImplementation(TranslationPolicyMixin):
527+ implements(ITranslationPolicy)
528+
529+ translationgroup = None
530+
531+ translationpermission = TranslationPermission.OPEN
532+
533+
534+class TestTranslationPolicy(TestCaseWithFactory):
535+ layer = ZopelessDatabaseLayer
536+
537+ def setUp(self):
538+ super(TestTranslationPolicy, self).setUp()
539+ self.policy = TranslationPolicyImplementation()
540+
541+ def _makeParentPolicy(self):
542+ """Create a policy that `self.policy` inherits from."""
543+ parent = TranslationPolicyImplementation()
544+ self.policy.getInheritedTranslationPolicy = FakeMethod(result=parent)
545+ return parent
546+
547+ def _makeTranslationGroups(self, count):
548+ """Return a list of `count` freshly minted `TranslationGroup`s."""
549+ return [
550+ self.factory.makeTranslationGroup() for number in xrange(count)]
551+
552+ def _makeTranslator(self, language, for_policy=None):
553+ """Create a translator for a policy object.
554+
555+ Default is `self.policy`. Creates a translation group if necessary.
556+ """
557+ if for_policy is None:
558+ for_policy = self.policy
559+ if for_policy.translationgroup is None:
560+ for_policy.translationgroup = self.factory.makeTranslationGroup()
561+ person = self.factory.makePerson()
562+ getUtility(ITranslatorSet).new(
563+ for_policy.translationgroup, language, person, None)
564+ return person
565+
566+ def _setPermissions(self, child_permission, parent_permission):
567+ """Set `TranslationPermission`s for `self.policy` and its parent."""
568+ self.policy.translationpermission = child_permission
569+ self.policy.getInheritedTranslationPolicy().translationpermission = (
570+ parent_permission)
571+
572+ def test_hasSpecialTranslationPrivileges_for_regular_joe(self):
573+ # A logged-in user has no special translationprivileges by
574+ # default.
575+ joe = self.factory.makePerson()
576+ self.assertFalse(self.policy._hasSpecialTranslationPrivileges(joe))
577+
578+ def test_hasSpecialTranslationPrivileges_for_admin(self):
579+ # Admins have special translation privileges.
580+ admin = self.factory.makePerson()
581+ getUtility(ILaunchpadCelebrities).admin.addMember(admin, admin)
582+ self.assertTrue(self.policy._hasSpecialTranslationPrivileges(admin))
583+
584+ def test_hasSpecialTranslationPrivileges_for_translations_owner(self):
585+ # A policy may define a "translations owner" who also gets
586+ # special translation privileges.
587+ self.policy.isTranslationsOwner = FakeMethod(result=True)
588+ owner = self.factory.makePerson()
589+ self.assertTrue(self.policy._hasSpecialTranslationPrivileges(owner))
590+
591+ def test_canTranslate(self):
592+ # A user who has declined the licensing agreement can't
593+ # translate. Someone who has agreed, or not made a decision
594+ # yet, can.
595+ user = self.factory.makePerson()
596+ translations_user = ITranslationsPerson(user)
597+
598+ self.assertTrue(self.policy._canTranslate(user))
599+
600+ translations_user.translations_relicensing_agreement = True
601+ self.assertTrue(self.policy._canTranslate(user))
602+
603+ translations_user.translations_relicensing_agreement = False
604+ self.assertFalse(self.policy._canTranslate(user))
605+
606+ def test_getTranslationGroups_returns_translation_group(self):
607+ # In the simple case, getTranslationGroup simply returns the
608+ # policy implementation's translation group.
609+ self.assertEqual([], self.policy.getTranslationGroups())
610+ self.policy.translationgroup = self.factory.makeTranslationGroup()
611+ self.assertEqual(
612+ [self.policy.translationgroup],
613+ self.policy.getTranslationGroups())
614+
615+ def test_getTranslationGroups_enumerates_groups_inherited_first(self):
616+ parent = self._makeParentPolicy()
617+ groups = self._makeTranslationGroups(2)
618+ parent.translationgroup = groups[0]
619+ self.policy.translationgroup = groups[1]
620+ self.assertEqual(groups, self.policy.getTranslationGroups())
621+
622+ def test_getTranslationGroups_inheritance_is_asymmetric(self):
623+ parent = self._makeParentPolicy()
624+ groups = self._makeTranslationGroups(2)
625+ parent.translationgroup = groups[0]
626+ self.policy.translationgroup = groups[1]
627+ self.assertEqual(groups[:1], parent.getTranslationGroups())
628+
629+ def test_getTranslationGroups_eliminates_duplicates(self):
630+ parent = self._makeParentPolicy()
631+ groups = self._makeTranslationGroups(1)
632+ parent.translationgroup = groups[0]
633+ self.policy.translationgroup = groups[0]
634+ self.assertEqual(groups, self.policy.getTranslationGroups())
635+
636+ def test_getTranslators_without_groups_returns_empty_list(self):
637+ language = self.factory.makeLanguage()
638+ self.assertEqual([], self.policy.getTranslators(language))
639+
640+ def test_getTranslators_returns_group_even_without_translators(self):
641+ self.policy.translationgroup = self.factory.makeTranslationGroup()
642+ self.assertEqual(
643+ [(self.policy.translationgroup, None, None)],
644+ self.policy.getTranslators(self.factory.makeLanguage()))
645+
646+ def test_getTranslators_returns_translator(self):
647+ language = self.factory.makeLanguage()
648+ language_translator = self._makeTranslator(language)
649+ translators = self.policy.getTranslators(language)
650+ self.assertEqual(1, len(translators))
651+ group, translator, person = translators[0]
652+ self.assertEqual(self.policy.translationgroup, group)
653+ self.assertEqual(
654+ self.policy.translationgroup, translator.translationgroup)
655+ self.assertEqual(person, translator.translator)
656+ self.assertEqual(language, translator.language)
657+ self.assertEqual(language_translator, person)
658+
659+ def test_getEffectiveTranslationPermission_returns_permission(self):
660+ # In the basic case, getEffectiveTranslationPermission just
661+ # returns the policy's translation permission.
662+ self.policy.translationpermission = TranslationPermission.CLOSED
663+ self.assertEqual(
664+ self.policy.translationpermission,
665+ self.policy.getEffectiveTranslationPermission())
666+
667+ def test_getEffectiveTranslationPermission_returns_maximum(self):
668+ # When combining permissions, getEffectiveTranslationPermission
669+ # returns the one with the highest numerical value.
670+ parent = self._makeParentPolicy()
671+ for child_permission in TranslationPermission.items:
672+ for parent_permission in TranslationPermission.items:
673+ self._setPermissions(child_permission, parent_permission)
674+ stricter = max(child_permission, parent_permission)
675+ self.assertEqual(
676+ stricter, self.policy.getEffectiveTranslationPermission())
677+
678+ def test_maximum_permission_is_strictest(self):
679+ # The TranslationPermissions are ordered from loosest to
680+ # strictest, so the maximum is always the strictest.
681+ self.assertEqual(TranslationPermission.STRUCTURED, max(
682+ TranslationPermission.OPEN, TranslationPermission.STRUCTURED))
683+ self.assertEqual(TranslationPermission.RESTRICTED, max(
684+ TranslationPermission.STRUCTURED,
685+ TranslationPermission.RESTRICTED))
686+ self.assertEqual(TranslationPermission.CLOSED, max(
687+ TranslationPermission.RESTRICTED,
688+ TranslationPermission.CLOSED))

Subscribers

People subscribed via source and target branches