Merge lp:~jtv/launchpad/recife-policy-invites-allows into lp:~launchpad/launchpad/recife

Proposed by Jeroen T. Vermeulen on 2010-11-09
Status: Merged
Merged at revision: 9189
Proposed branch: lp:~jtv/launchpad/recife-policy-invites-allows
Merge into: lp:~launchpad/launchpad/recife
Prerequisite: lp:~jtv/launchpad/recife-pre-cleanups
Diff against target: 854 lines (+470/-204)
11 files modified
lib/lp/registry/model/distribution.py (+26/-0)
lib/lp/registry/model/product.py (+9/-0)
lib/lp/registry/model/projectgroup.py (+10/-0)
lib/lp/translations/interfaces/potemplate.py (+5/-0)
lib/lp/translations/interfaces/translationpolicy.py (+72/-0)
lib/lp/translations/model/pofile.py (+11/-171)
lib/lp/translations/model/potemplate.py (+14/-5)
lib/lp/translations/model/translationpolicy.py (+82/-0)
lib/lp/translations/tests/test_potemplate.py (+12/-0)
lib/lp/translations/tests/test_translationpolicy.py (+222/-0)
lib/lp/translations/utilities/translation_import.py (+7/-28)
To merge this branch: bzr merge lp:~jtv/launchpad/recife-policy-invites-allows
Reviewer Review Type Date Requested Status
Māris Fogels (community) 2010-11-09 Approve on 2010-11-09
Review via email: mp+40438@code.launchpad.net

Commit Message

Recife model's translations access and sharing policies.

Description of the Change

This is a complete overhaul of the translation permissions code. Be warned. It's probably oversized, too, though I've been splitting off lots of work into separate branches to keep sizes down.

The merge target is Recife, a feature branch we've been working on all year.

I hope the best explanation I can give for the new model is in the code itself. But to set the right frame of reference:

 * Whether a user can edit a translation, or enter suggestions there was governed by code in the pofile module.

 * That code was at least 5 years old, and not unit-tested directly until I replaced the doctest with more systematic unit tests recently.

 * Throughout the ages, as empires rose and fell and continents collided and drifted apart, the human race kept piling more checks into the access model but also began using it in more and different ways. Scientists today are hard-pressed to describe exactly what these checks are and aren't meant to be responsible for.

 * My apologies for the little David Attenborough speech there. No more National Geographic Channel for me.

 * Unfortunately there are still two checks for read-only mode in the "access" checks for POFile. I'd like to clean those up later, but note that during a slow submission they can catch cases where the server goes read-only after the check at the beginning of the request, and so turn oopses into slightly less annoying exceptions.

 * We now need even more from the access checks: we need to know whether a translation made in Ubuntu (by a particular person and in a particular language) should be shared with a linked upstream product (if any) or vice versa. We call this issue "sharing policy."

 * Sharing policy turns out not to depend on access permissions exactly. It touches on access rights, but is fundamentally a workflow choice.

 * Therefore in this branch I tease those two aspects of the access rights apart, and move them into TranslationPolicy. You'll also find sharing policy defined there.

I actually duplicate a lot of testing here. The POFile permissions test I added recently covers much of the same ground as the tests you see here. That was absolutely necessary to guard continuity during these changes. I could possibly slim them down now that we have direct tests on the policy system. On the other hand, the POFile permissions test is structured in a very different way and both seem worthwhile. They may complement each other somewhat. Also of course, I'm not throwing any more changes into this diff!

There are bits of lint left, most of which I intend to clean up after review (together with automated copyright updates and import re-sorting):
{{{

./lib/lp/registry/model/distribution.py
    1236: Line exceeds 78 characters.
./lib/lp/translations/model/pofile.py
      72: 'TranslationPermission' imported but unused
     102: 'ITranslationsPerson' imported but unused
./lib/lp/translations/utilities/translation_import.py
      36: 'ITranslationSideTraitsSet' imported but unused
      35: 'IPOTemplateSet' imported but unused
      36: 'TranslationSide' imported but unused
}}}

The distribution one however is not one I'd like to touch. Better for Registry people to deal with it.

Jeroen

To post a comment you must log in.
Māris Fogels (mars) wrote :

Hi Jeroen,

This change looks good. The code and tests are clear, even for someone like myself who is not entirely familiar with the model. In all cases the new code is easier to read than what it replaced.

I mostly looked at changes to test coverage, since you said you overhauled the doctests in preference to unit tests. Given that, are the changes to the translationtarget property tested? What I see represents a big change to the method's return type and type checking.

Similarly, share_with_other_side() changed - is it tested elsewhere as well?

I think the change looks good enough to land as-is, if you want. r=mars

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

Thanks for going the extra mile (on such a big review) and looking that up! It turns out that one of the two, POTemplate.translationtarget, was not covered by unit tests. I fixed that.

Jeroen

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/registry/model/distribution.py'
2--- lib/lp/registry/model/distribution.py 2010-11-09 06:55:07 +0000
3+++ lib/lp/registry/model/distribution.py 2010-11-10 04:40:00 +0000
4@@ -1753,6 +1753,32 @@
5 # see: https://bugs.launchpad.net/soyuz/+bug/246200
6 return results.any() != None
7
8+ def sharesTranslationsWithOtherSide(self, person, language,
9+ sourcepackage=None,
10+ purportedly_upstream=False):
11+ """See `ITranslationPolicy`."""
12+ assert sourcepackage is not None, (
13+ "Translations sharing policy requires a SourcePackage.")
14+
15+ sharing_productseries = sourcepackage.productseries
16+ if sharing_productseries is None:
17+ # There is no known upstream series. Take the uploader's
18+ # word for whether these are upstream translations (in which
19+ # case they're shared) or not.
20+ # What are the consequences if that value is incorrect? In
21+ # the case where translations from upstream are purportedly
22+ # from Ubuntu, we miss a chance at sharing when the package
23+ # is eventually matched up with a productseries. An import
24+ # or sharing-script run will fix that. In the case where
25+ # Ubuntu translations are purportedly from upstream, an
26+ # import can fix it once a productseries is selected; or a
27+ # merge done by a script will give precedence to the Product
28+ # translations for upstream.
29+ return purportedly_upstream
30+
31+ upstream_product = sharing_productseries.product
32+ return upstream_product.invitesTranslationEdits(person, language)
33+
34
35 class DistributionSet:
36 """This class is to deal with Distribution related stuff"""
37
38=== modified file 'lib/lp/registry/model/product.py'
39--- lib/lp/registry/model/product.py 2010-11-09 06:55:07 +0000
40+++ lib/lp/registry/model/product.py 2010-11-10 04:40:00 +0000
41@@ -1059,6 +1059,15 @@
42 # policy from its ProjectGroup, if any.
43 return self.project
44
45+ def sharesTranslationsWithOtherSide(self, person, language,
46+ sourcepackage=None,
47+ purportedly_upstream=False):
48+ """See `ITranslationPolicy`."""
49+ assert sourcepackage is None, "Got a SourcePackage for a Product!"
50+ # Product translations are considered upstream. They are
51+ # automatically shared.
52+ return True
53+
54 @property
55 def has_any_specifications(self):
56 """See `IHasSpecifications`."""
57
58=== modified file 'lib/lp/registry/model/projectgroup.py'
59--- lib/lp/registry/model/projectgroup.py 2010-11-09 06:55:07 +0000
60+++ lib/lp/registry/model/projectgroup.py 2010-11-10 04:40:00 +0000
61@@ -208,6 +208,16 @@
62 # return not self.translatables().is_empty()
63 return self.translatables().count() != 0
64
65+ def sharesTranslationsWithOtherSide(self, person, language,
66+ sourcepackage=None,
67+ purportedly_upstream=False):
68+ """See `ITranslationPolicy`."""
69+ assert sourcepackage is None, (
70+ "Got a SourcePackage for a ProjectGroup!")
71+ # ProjectGroup translations are considered upstream. They are
72+ # automatically shared.
73+ return True
74+
75 def has_branches(self):
76 """ See `IProjectGroup`."""
77 return not self.getBranches().is_empty()
78
79=== modified file 'lib/lp/translations/interfaces/potemplate.py'
80--- lib/lp/translations/interfaces/potemplate.py 2010-11-09 06:44:07 +0000
81+++ lib/lp/translations/interfaces/potemplate.py 2010-11-10 04:40:00 +0000
82@@ -34,6 +34,7 @@
83 from canonical.launchpad.interfaces.librarian import ILibraryFileAlias
84 from lp.app.errors import NotFoundError
85 from lp.registry.interfaces.distribution import IDistribution
86+from lp.registry.interfaces.sourcepackage import ISourcePackage
87 from lp.registry.interfaces.sourcepackagename import ISourcePackageName
88 from lp.services.fields import PersonChoice
89 from lp.translations.interfaces.rosettastats import IRosettaStats
90@@ -168,6 +169,10 @@
91 required=False,
92 vocabulary="SourcePackageName")
93
94+ sourcepackage = Reference(
95+ ISourcePackage, title=u"Source package this template is for, if any.",
96+ required=False, readonly=True)
97+
98 from_sourcepackagename = Choice(
99 title=_("From Source Package Name"),
100 description=_(
101
102=== modified file 'lib/lp/translations/interfaces/translationpolicy.py'
103--- lib/lp/translations/interfaces/translationpolicy.py 2010-11-09 15:12:40 +0000
104+++ lib/lp/translations/interfaces/translationpolicy.py 2010-11-10 04:40:00 +0000
105@@ -22,6 +22,14 @@
106 add suggestions. (The ability to edit also implies the ability to
107 enter suggestions). Everyone else is allowed only to view the
108 translations.
109+
110+ The policy can "invite" the user to edit or suggest; or it can
111+ merely "allow" them to. Whoever is invited is also allowed, but
112+ administrators and certain other special users may be allowed
113+ without actually being invited.
114+
115+ The invitation is based purely on the access model configured by the
116+ user: translation team and translation policy.
117 """
118
119 translationgroup = Choice(
120@@ -79,3 +87,67 @@
121 `self.translationpermission` and any inherited
122 `TranslationPermission`.
123 """
124+
125+ def invitesTranslationEdits(person, language):
126+ """Does this policy invite `person` to edit translations?
127+
128+ The decision is based on the chosen `TranslationPermission`,
129+ `TranslationGroup`(s), the presence of a translation team, and
130+ `person`s membership of the translation team.
131+
132+ As one extreme, the OPEN model invites editing by anyone. The
133+ opposite extreme is CLOSED, which invites editing only by
134+ members of the applicable translation team.
135+
136+ :param person: The user.
137+ :type person: IPerson
138+ :param language: The language to translate to. This will be
139+ used to look up the applicable translation team(s).
140+ :type language: ILanguage
141+ """
142+
143+ def invitesTranslationSuggestions(person, language):
144+ """Does this policy invite `person` to enter suggestions?
145+
146+ Similar to `invitesTranslationEdits`, but for the activity of
147+ entering suggestions. This carries less risk, so generally a
148+ wider public is invited to do this than to edit.
149+ """
150+
151+ def allowsTranslationEdits(person, language):
152+ """Is `person` allowed to edit translations to `language`?
153+
154+ Similar to `invitesTranslationEdits`, except administrators and
155+ in the case of Product translations, owners of the product are
156+ always allowed even if they are not invited.
157+ """
158+
159+ def allowsTranslationSuggestions(person, language):
160+ """Is `person` allowed to enter suggestions for `language`?
161+
162+ Similar to `invitesTranslationSuggestions, except administrators
163+ and in the case of Product translations, owners of the product
164+ are always allowed even if they are not invited.
165+ """
166+
167+ def sharesTranslationsWithOtherSide(person, language,
168+ sourcepackage=None,
169+ purportedly_upstream=False):
170+ """Should translations be shared across `TranslationSide`s?
171+
172+ Should translations to this object, as reviewed by `person`,
173+ into `language` be shared with the other `TranslationSide`?
174+
175+ The answer depends on whether the user is invited to edit the
176+ translations on the other side. Administrators and other
177+ specially privileged users are allowed to do that, but that
178+ does not automatically mean that their translations should be
179+ shared there.
180+
181+ :param person: The `Person` providing translations.
182+ :param language: The `Language` being translated to.
183+ :param sourcepackage: When translating a `Distribution`, the
184+ `SourcePackage` that is being translated.
185+ :param purportedly_upstream: Whether `person` provides the
186+ translations in question as coming from upstream.
187+ """
188
189=== modified file 'lib/lp/translations/model/pofile.py'
190--- lib/lp/translations/model/pofile.py 2010-11-09 09:46:20 +0000
191+++ lib/lp/translations/model/pofile.py 2010-11-10 04:40:00 +0000
192@@ -69,10 +69,7 @@
193 from canonical.launchpad.webapp.publisher import canonical_url
194 from lp.registry.interfaces.person import validate_public_person
195 from lp.services.propertycache import cachedproperty
196-from lp.translations.enums import (
197- RosettaImportStatus,
198- TranslationPermission,
199- )
200+from lp.translations.enums import RosettaImportStatus
201 from lp.translations.interfaces.pofile import (
202 IPOFile,
203 IPOFileSet,
204@@ -99,7 +96,6 @@
205 TranslationValidationStatus,
206 )
207 from lp.translations.interfaces.translations import TranslationConstants
208-from lp.translations.interfaces.translationsperson import ITranslationsPerson
209 from lp.translations.model.pomsgid import POMsgID
210 from lp.translations.model.potmsgset import POTMsgSet
211 from lp.translations.model.translatablemessage import TranslatableMessage
212@@ -117,170 +113,6 @@
213 )
214
215
216-def _check_translation_perms(permission, translators, person):
217- """Return True or False dependening on whether the person is part of the
218- right group of translators, and the permission on the relevant project,
219- product or distribution.
220-
221- :param permission: The kind of TranslationPermission.
222- :param translators: The list of official translators for the
223- product/project/distribution.
224- :param person: The person that we want to check if has translation
225- permissions.
226- """
227- # Let's determine if the person is part of a designated translation team
228- is_designated_translator = False
229- # XXX sabdfl 2005-05-25:
230- # This code could be improved when we have implemented CrowdControl.
231- for translator in translators:
232- if person.inTeam(translator):
233- is_designated_translator = True
234- break
235-
236- # have a look at the applicable permission policy
237- if permission == TranslationPermission.OPEN:
238- # if the translation policy is "open", then yes, anybody is an
239- # editor of any translation
240- return True
241- elif permission == TranslationPermission.STRUCTURED:
242- # in the case of a STRUCTURED permission, designated translators
243- # can edit, unless there are no translators, in which case
244- # anybody can translate
245- if len(translators) > 0:
246- # when there are designated translators, only they can edit
247- if is_designated_translator is True:
248- return True
249- else:
250- # since there are no translators, anyone can edit
251- return True
252- elif permission in (TranslationPermission.RESTRICTED,
253- TranslationPermission.CLOSED):
254- # if the translation policy is "restricted" or "closed", then check if
255- # the person is in the set of translators
256- if is_designated_translator:
257- return True
258- else:
259- raise NotImplementedError('Unknown permission %s' % permission.name)
260-
261- # ok, thats all we can check, and so we must assume the answer is no
262- return False
263-
264-
265-def _person_has_not_licensed_translations(person):
266- """Whether a person has declined to BSD-license their translations."""
267- t_p = ITranslationsPerson(person)
268- if (t_p.translations_relicensing_agreement is not None and
269- t_p.translations_relicensing_agreement is False):
270- return True
271- else:
272- return False
273-
274-
275-def is_admin(user):
276- """Is `user` an admin or Translations admin?"""
277- celebs = getUtility(ILaunchpadCelebrities)
278- return user.inTeam(celebs.admin) or user.inTeam(celebs.rosetta_experts)
279-
280-
281-def is_product_owner(user, potemplate):
282- """Is `user` the owner of a `Product` that `potemplate` belongs to?
283-
284- A product's owners have edit rights on the product's translations.
285- """
286- productseries = potemplate.productseries
287- if productseries is None:
288- return False
289-
290- return user.inTeam(productseries.product.owner)
291-
292-
293-def _can_edit_translations(pofile, person):
294- """Say if a person is able to edit existing translations.
295-
296- Return True or False indicating whether the person is allowed
297- to edit these translations.
298-
299- Admins and Rosetta experts are always able to edit any translation.
300- If the `IPOFile` is for an `IProductSeries`, the owner of the `IProduct`
301- has also permissions.
302- Any other mortal will have rights depending on if he/she is on the right
303- translation team for the given `IPOFile`.translationpermission and the
304- language associated with this `IPOFile`.
305- """
306- if person is None:
307- # Anonymous users can't edit anything.
308- return False
309-
310- if is_read_only():
311- # Nothing can be edited in read-only mode.
312- return False
313-
314- # XXX Carlos Perello Marin 2006-02-07 bug=4814:
315- # We should not check the permissions here but use the standard
316- # security system.
317-
318- # Rosetta experts and admins can always edit translations.
319- if is_admin(person):
320- return True
321-
322- # The owner of the product is also able to edit translations.
323- if is_product_owner(person, pofile.potemplate):
324- return True
325-
326- # If a person has decided not to license their translations under BSD
327- # license they can't edit translations.
328- if _person_has_not_licensed_translations(person):
329- return False
330-
331- # Finally, check whether the user is a member of the translation team.
332- translators = [t.translator for t in pofile.translators]
333- return _check_translation_perms(
334- pofile.translationpermission, translators, person)
335-
336-
337-def _can_add_suggestions(pofile, person):
338- """Whether a person is able to add suggestions.
339-
340- Besides people who have permission to edit the translation, this
341- includes any logged-in user for translations in STRUCTURED mode, and
342- any logged-in user for translations in RESTRICTED mode that have a
343- translation team assigned.
344- """
345- if is_read_only():
346- # No suggestions can be added in read-only mode.
347- return False
348-
349- if person is None:
350- return False
351-
352- # If a person has decided not to license their translations under BSD
353- # license they can't edit translations.
354- if _person_has_not_licensed_translations(person):
355- return False
356-
357- if _can_edit_translations(pofile, person):
358- return True
359-
360- if pofile.translationpermission == TranslationPermission.OPEN:
361- # We would return True here, except OPEN mode already allows
362- # anyone to edit (see above).
363- raise AssertionError(
364- "Translation is OPEN, but user is not allowed to edit.")
365- elif pofile.translationpermission == TranslationPermission.STRUCTURED:
366- return True
367- elif pofile.translationpermission == TranslationPermission.RESTRICTED:
368- # Only allow suggestions if there is someone to review them.
369- groups = pofile.potemplate.translationgroups
370- for group in groups:
371- if group.query_translator(pofile.language):
372- return True
373- return False
374- elif pofile.translationpermission == TranslationPermission.CLOSED:
375- return False
376-
377- raise AssertionError("Unknown translation mode.")
378-
379-
380 class POFileMixIn(RosettaStats):
381 """Base class for `POFile` and `DummyPOFile`.
382
383@@ -306,11 +138,19 @@
384
385 def canEditTranslations(self, person):
386 """See `IPOFile`."""
387- return _can_edit_translations(self, person)
388+ if is_read_only():
389+ # Nothing can be edited in read-only mode.
390+ return False
391+ policy = self.potemplate.getTranslationPolicy()
392+ return policy.allowsTranslationEdits(person, self.language)
393
394 def canAddSuggestions(self, person):
395 """See `IPOFile`."""
396- return _can_add_suggestions(self, person)
397+ if is_read_only():
398+ # No data can be entered in read-only mode.
399+ return False
400+ policy = self.potemplate.getTranslationPolicy()
401+ return policy.allowsTranslationSuggestions(person, self.language)
402
403 def getHeader(self):
404 """See `IPOFile`."""
405
406=== modified file 'lib/lp/translations/model/potemplate.py'
407--- lib/lp/translations/model/potemplate.py 2010-11-09 14:25:41 +0000
408+++ lib/lp/translations/model/potemplate.py 2010-11-10 04:40:00 +0000
409@@ -366,16 +366,25 @@
410 clauseTables=['POFile'],
411 distinct=True).count()
412
413+ @cachedproperty
414+ def sourcepackage(self):
415+ """See `IPOTemplate`."""
416+ # Avoid circular imports
417+ from lp.registry.model.sourcepackage import SourcePackage
418+
419+ if self.distroseries is None:
420+ return None
421+ return SourcePackage(
422+ distroseries=self.distroseries,
423+ sourcepackagename=self.sourcepackagename)
424+
425 @property
426 def translationtarget(self):
427 """See `IPOTemplate`."""
428 if self.productseries is not None:
429 return self.productseries
430- elif self.distroseries is not None:
431- from lp.registry.model.sourcepackage import SourcePackage
432- return SourcePackage(distroseries=self.distroseries,
433- sourcepackagename=self.sourcepackagename)
434- raise AssertionError('Unknown POTemplate translation target')
435+ else:
436+ return self.sourcepackage
437
438 def getHeader(self):
439 """See `IPOTemplate`."""
440
441=== modified file 'lib/lp/translations/model/translationpolicy.py'
442--- lib/lp/translations/model/translationpolicy.py 2010-11-08 08:58:38 +0000
443+++ lib/lp/translations/model/translationpolicy.py 2010-11-10 04:40:00 +0000
444@@ -17,11 +17,28 @@
445 from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
446 from canonical.launchpad.interfaces.lpstorm import IStore
447 from lp.registry.model.person import Person
448+from lp.translations.enums import TranslationPermission
449 from lp.translations.interfaces.translationsperson import ITranslationsPerson
450 from lp.translations.model.translationgroup import TranslationGroup
451 from lp.translations.model.translator import Translator
452
453
454+def has_translators(translators_list):
455+ """Did `getTranslators` find any translators?"""
456+ for group, translator, team in translators_list:
457+ if team is not None:
458+ return True
459+ return False
460+
461+
462+def is_in_one_of_translators(translators_list, person):
463+ """Is `person` a member of one of the entries in `getTranslators`?"""
464+ for group, translator, team in translators_list:
465+ if team is not None and person.inTeam(team):
466+ return True
467+ return False
468+
469+
470 class TranslationPolicyMixin:
471 """Implementation mixin for `ITranslationPolicy`."""
472
473@@ -102,3 +119,68 @@
474 return max([
475 self.translationpermission,
476 inherited.getEffectiveTranslationPermission()])
477+
478+ def invitesTranslationEdits(self, person, language):
479+ """See `ITranslationPolicy`."""
480+ if person is None:
481+ return False
482+
483+ model = self.getEffectiveTranslationPermission()
484+ if model == TranslationPermission.OPEN:
485+ # Open permissions invite all contributions.
486+ return True
487+
488+ translators = self.getTranslators(language)
489+ if model == TranslationPermission.STRUCTURED:
490+ # Structured permissions act like Open if no translators
491+ # have been assigned for the language.
492+ if not has_translators(translators):
493+ return True
494+
495+ # Translation-team members are always invited to edit.
496+ return is_in_one_of_translators(translators, person)
497+
498+ def invitesTranslationSuggestions(self, person, language):
499+ """See `ITranslationPolicy`."""
500+ if person is None:
501+ return False
502+
503+ model = self.getEffectiveTranslationPermission()
504+
505+ # These models always invite suggestions from anyone.
506+ welcoming_models = [
507+ TranslationPermission.OPEN,
508+ TranslationPermission.STRUCTURED,
509+ ]
510+ if model in welcoming_models:
511+ return True
512+
513+ translators = self.getTranslators(language)
514+ if model == TranslationPermission.RESTRICTED:
515+ if has_translators(translators):
516+ # Restricted invites any user's suggestions as long as
517+ # there is a translation team to handle them.
518+ return True
519+
520+ # Translation-team members are always invited to suggest.
521+ return is_in_one_of_translators(translators, person)
522+
523+ def allowsTranslationEdits(self, person, language):
524+ """See `ITranslationPolicy`."""
525+ if person is None:
526+ return False
527+ if self._hasSpecialTranslationPrivileges(person):
528+ return True
529+ return (
530+ self._canTranslate(person) and
531+ self.invitesTranslationEdits(person, language))
532+
533+ def allowsTranslationSuggestions(self, person, language):
534+ """See `ITranslationPolicy`."""
535+ if person is None:
536+ return False
537+ if self._hasSpecialTranslationPrivileges(person):
538+ return True
539+ return (
540+ self._canTranslate(person) and
541+ self.invitesTranslationSuggestions(person, language))
542
543=== modified file 'lib/lp/translations/tests/test_potemplate.py'
544--- lib/lp/translations/tests/test_potemplate.py 2010-10-19 12:52:33 +0000
545+++ lib/lp/translations/tests/test_potemplate.py 2010-11-10 04:40:00 +0000
546@@ -139,6 +139,18 @@
547 self.assertEqual(1, len(karma_events))
548 self.assertEqual(action, karma_events[0].action.name)
549
550+ def test_translationtarget_can_be_productseries(self):
551+ productseries = self.factory.makeProductSeries()
552+ template = self.factory.makePOTemplate(productseries=productseries)
553+ self.assertEqual(productseries, template.translationtarget)
554+
555+ def test_translationtarget_can_be_sourcepackage(self):
556+ package = self.factory.makeSourcePackage()
557+ template = self.factory.makePOTemplate(
558+ distroseries=package.distroseries,
559+ sourcepackagename=package.sourcepackagename)
560+ self.assertEqual(package, template.translationtarget)
561+
562
563 class EquivalenceClassTestMixin:
564 """Helper for POTemplate equivalence class tests."""
565
566=== modified file 'lib/lp/translations/tests/test_translationpolicy.py'
567--- lib/lp/translations/tests/test_translationpolicy.py 2010-11-09 14:25:41 +0000
568+++ lib/lp/translations/tests/test_translationpolicy.py 2010-11-10 04:40:00 +0000
569@@ -7,6 +7,7 @@
570
571 from zope.component import getUtility
572 from zope.interface import implements
573+from zope.security.proxy import removeSecurityProxy
574
575 from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
576 from canonical.testing.layers import ZopelessDatabaseLayer
577@@ -187,3 +188,224 @@
578 self.assertEqual(TranslationPermission.CLOSED, max(
579 TranslationPermission.RESTRICTED,
580 TranslationPermission.CLOSED))
581+
582+ def test_nobodies_stay_out(self):
583+ # We neither allow nor invite suggestions or edits by anonymous
584+ # users.
585+ language = self.factory.makeLanguage()
586+ self.assertFalse(self.policy.invitesTranslationEdits(None, language))
587+ self.assertFalse(
588+ self.policy.invitesTranslationSuggestions(None, language))
589+ self.assertFalse(self.policy.allowsTranslationEdits(None, language))
590+ self.assertFalse(
591+ self.policy.allowsTranslationSuggestions(None, language))
592+
593+ def test_privileged_users_allowed_but_not_invited(self):
594+ # Specially privileged users such as administrators and
595+ # "translations owners" can enter suggestions and edit
596+ # translations, but are not particularly invited to do so.
597+ owner = self.factory.makePerson()
598+ language = self.factory.makeLanguage()
599+ self.policy.translationpermission = TranslationPermission.CLOSED
600+ self.policy.isTranslationsOwner = FakeMethod(result=True)
601+ self.assertFalse(self.policy.invitesTranslationEdits(owner, language))
602+ self.assertFalse(
603+ self.policy.invitesTranslationSuggestions(owner, language))
604+ self.assertTrue(self.policy.allowsTranslationEdits(owner, language))
605+ self.assertTrue(
606+ self.policy.allowsTranslationSuggestions(owner, language))
607+
608+ def test_open_invites_anyone(self):
609+ # The OPEN model invites anyone to enter suggestions or even
610+ # edit translations.
611+ joe = self.factory.makePerson()
612+ language = self.factory.makeLanguage()
613+ self.policy.translationpermission = TranslationPermission.OPEN
614+ self.assertTrue(self.policy.invitesTranslationEdits(joe, language))
615+ self.assertTrue(
616+ self.policy.invitesTranslationSuggestions(joe, language))
617+
618+ def test_translation_team_members_are_invited(self):
619+ # Members of a translation team are invited (and thus allowed)
620+ # to enter suggestions for or edit translations covered by the
621+ # translation team.
622+ language = self.factory.makeLanguage()
623+ translator = self._makeTranslator(language)
624+ for permission in TranslationPermission.items:
625+ self.policy.translationpermission = permission
626+ self.assertTrue(
627+ self.policy.invitesTranslationEdits(translator, language))
628+ self.assertTrue(
629+ self.policy.invitesTranslationSuggestions(
630+ translator, language))
631+
632+ def test_structured_is_open_for_untended_translations(self):
633+ # Without a translation team, STRUCTURED is like OPEN.
634+ joe = self.factory.makePerson()
635+ language = self.factory.makeLanguage()
636+ self.policy.translationpermission = TranslationPermission.STRUCTURED
637+ self.assertTrue(self.policy.invitesTranslationEdits(joe, language))
638+ self.assertTrue(
639+ self.policy.invitesTranslationSuggestions(joe, language))
640+
641+ def test_restricted_is_closed_for_untended_translations(self):
642+ # Without a translation team, RESTRICTED is like CLOSED.
643+ joe = self.factory.makePerson()
644+ language = self.factory.makeLanguage()
645+ self.policy.translationpermission = TranslationPermission.RESTRICTED
646+ self.assertFalse(self.policy.invitesTranslationEdits(joe, language))
647+ self.assertFalse(
648+ self.policy.invitesTranslationSuggestions(joe, language))
649+
650+ def test_structured_and_restricted_for_tended_translations(self):
651+ # If there's a translation team, STRUCTURED and RESTRICTED both
652+ # invite suggestions (but not editing) by non-members.
653+ joe = self.factory.makePerson()
654+ language = self.factory.makeLanguage()
655+ self._makeTranslator(language)
656+ intermediate_permissions = [
657+ TranslationPermission.STRUCTURED,
658+ TranslationPermission.RESTRICTED,
659+ ]
660+ for permission in intermediate_permissions:
661+ self.policy.translationpermission = permission
662+ self.assertFalse(
663+ self.policy.invitesTranslationEdits(joe, language))
664+ self.assertTrue(
665+ self.policy.invitesTranslationSuggestions(joe, language))
666+
667+ def test_closed_invites_nobody_for_untended_translations(self):
668+ # The CLOSED model does not invite anyone for untended
669+ # translations.
670+ joe = self.factory.makePerson()
671+ language = self.factory.makeLanguage()
672+ self.policy.translationpermission = TranslationPermission.CLOSED
673+
674+ self.assertFalse(self.policy.invitesTranslationEdits(joe, language))
675+ self.assertFalse(
676+ self.policy.invitesTranslationSuggestions(joe, language))
677+
678+ def test_closed_does_not_invite_nonmembers_for_tended_translations(self):
679+ # The CLOSED model invites nobody outside the translation team.
680+ joe = self.factory.makePerson()
681+ language = self.factory.makeLanguage()
682+ self._makeTranslator(language)
683+ self.policy.translationpermission = TranslationPermission.CLOSED
684+
685+ self.assertFalse(self.policy.invitesTranslationEdits(joe, language))
686+ self.assertFalse(
687+ self.policy.invitesTranslationSuggestions(joe, language))
688+
689+ def test_untended_translation_means_no_team(self):
690+ # A translation is "untended" if there is no translation team,
691+ # even if there is a translation group.
692+ joe = self.factory.makePerson()
693+ language = self.factory.makeLanguage()
694+ self.policy.translationpermission = TranslationPermission.RESTRICTED
695+
696+ self.assertFalse(
697+ self.policy.invitesTranslationSuggestions(joe, language))
698+ self.policy.translationgroup = self.factory.makeTranslationGroup()
699+ self.assertFalse(
700+ self.policy.invitesTranslationSuggestions(joe, language))
701+
702+ def test_translation_can_be_tended_by_empty_team(self):
703+ # A translation that has an empty translation team is tended.
704+ joe = self.factory.makePerson()
705+ language = self.factory.makeLanguage()
706+ self.policy.translationgroup = self.factory.makeTranslationGroup()
707+ getUtility(ITranslatorSet).new(
708+ self.policy.translationgroup, language, self.factory.makeTeam(),
709+ None)
710+ self.policy.translationpermission = TranslationPermission.RESTRICTED
711+
712+ self.assertTrue(
713+ self.policy.invitesTranslationSuggestions(joe, language))
714+
715+
716+class TestTranslationsOwners(TestCaseWithFactory):
717+ """Who exactly are "translations owners"?
718+
719+ :ivar owner: A `Person` to be used as an owner of various things.
720+ """
721+ layer = ZopelessDatabaseLayer
722+
723+ def setUp(self):
724+ super(TestTranslationsOwners, self).setUp()
725+ self.owner = self.factory.makePerson()
726+
727+ def isTranslationsOwnerOf(self, pillar):
728+ """Is `self.owner` a translations owner of `pillar`?"""
729+ return removeSecurityProxy(pillar).isTranslationsOwner(self.owner)
730+
731+ def test_product_owners(self):
732+ # Product owners are "translations owners."
733+ product = self.factory.makeProduct(owner=self.owner)
734+ self.assertTrue(self.isTranslationsOwnerOf(product))
735+
736+ def test_projectgroup_owners(self):
737+ # ProjectGroup owners are not translations owners.
738+ project = self.factory.makeProject(owner=self.owner)
739+ self.assertFalse(self.isTranslationsOwnerOf(project))
740+
741+ def test_distribution_owners(self):
742+ # Distribution owners are not translations owners.
743+ distro = self.factory.makeDistribution(owner=self.owner)
744+ self.assertFalse(self.isTranslationsOwnerOf(distro))
745+
746+ def test_product_translationgroup_owners(self):
747+ # Translation group owners are not translations owners in the
748+ # case of Products.
749+ group = self.factory.makeTranslationGroup(owner=self.owner)
750+ product = self.factory.makeProject()
751+ product.translationgroup = group
752+ self.assertFalse(self.isTranslationsOwnerOf(product))
753+
754+ def test_distro_translationgroup_owners(self):
755+ # Translation group owners are not translations owners in the
756+ # case of Distributions.
757+ group = self.factory.makeTranslationGroup(owner=self.owner)
758+ distro = self.factory.makeDistribution()
759+ distro.translationgroup = group
760+ self.assertFalse(self.isTranslationsOwnerOf(distro))
761+
762+
763+class TestSharingPolicy(TestCaseWithFactory):
764+ """Test `ITranslationPolicy`'s sharing between Ubuntu and upstream."""
765+ layer = ZopelessDatabaseLayer
766+
767+ def setUp(self):
768+ super(TestSharingPolicy, self).setUp()
769+ self.user = self.factory.makePerson()
770+ self.language = self.factory.makeLanguage()
771+
772+ def _doesPackageShare(self, sourcepackage, from_upstream=False):
773+ """Does this `SourcePackage` share with upstream?"""
774+ distro = sourcepackage.distroseries.distribution
775+ return distro.sharesTranslationsWithOtherSide(
776+ self.user, self.language, sourcepackage=sourcepackage,
777+ purportedly_upstream=from_upstream)
778+
779+ def test_product_always_shares(self):
780+ product = self.factory.makeProduct()
781+ self.assertTrue(
782+ product.sharesTranslationsWithOtherSide(self.user, self.language))
783+
784+ def test_distribution_shares_only_if_invited(self):
785+ package = self.factory.makeSourcePackage()
786+ self.factory.makePackagingLink(
787+ sourcepackagename=package.sourcepackagename,
788+ distroseries=package.distroseries)
789+ product = package.productseries.product
790+
791+ product.translationpermission = TranslationPermission.OPEN
792+ self.assertTrue(self._doesPackageShare(package))
793+ product.translationpermission = TranslationPermission.CLOSED
794+ self.assertFalse(self._doesPackageShare(package))
795+
796+ def test_unlinked_package_shares_only_upstream_translations(self):
797+ package = self.factory.makeSourcePackage()
798+ distro = package.distroseries.distribution
799+ for from_upstream in [False, True]:
800+ self.assertEqual(
801+ from_upstream, self._doesPackageShare(package, from_upstream))
802
803=== modified file 'lib/lp/translations/utilities/translation_import.py'
804--- lib/lp/translations/utilities/translation_import.py 2010-11-09 09:46:20 +0000
805+++ lib/lp/translations/utilities/translation_import.py 2010-11-10 04:40:00 +0000
806@@ -32,11 +32,6 @@
807 )
808 from lp.services.propertycache import cachedproperty
809 from lp.translations.enums import RosettaImportStatus
810-from lp.translations.interfaces.potemplate import IPOTemplateSet
811-from lp.translations.interfaces.side import (
812- ITranslationSideTraitsSet,
813- TranslationSide,
814- )
815 from lp.translations.interfaces.translationexporter import (
816 ITranslationExporter,
817 )
818@@ -462,29 +457,13 @@
819 def share_with_other_side(self):
820 """Returns True if translations should be shared with the other side.
821 """
822- traits = getUtility(
823- ITranslationSideTraitsSet).getForTemplate(self.potemplate)
824- if traits.side == TranslationSide.UPSTREAM:
825- return True
826- # Maintainer uploads are always shared with Ubuntu.
827- if self.translation_import_queue_entry.by_maintainer:
828- return True
829- # Find the sharing POFile and check permissions.
830- productseries = self.potemplate.distroseries.getSourcePackage(
831- self.potemplate.sourcepackagename).productseries
832- if productseries is None:
833- return False
834- upstream_template = getUtility(IPOTemplateSet).getSubset(
835- productseries=productseries).getPOTemplateByName(
836- self.potemplate.name)
837- upstream_pofile = upstream_template.getPOFileByLang(
838- self.pofile.language.code)
839- if upstream_pofile is not None:
840- uploader_person = self.translation_import_queue_entry.importer
841- if upstream_pofile.canEditTranslations(uploader_person):
842- return True
843- # Deny the rest.
844- return False
845+ from_upstream = self.translation_import_queue_entry.by_maintainer
846+ potemplate = self.potemplate
847+ policy = potemplate.getTranslationPolicy()
848+ return policy.sharesTranslationsWithOtherSide(
849+ self.importer, self.pofile.language,
850+ sourcepackage=potemplate.sourcepackage,
851+ purportedly_upstream=from_upstream)
852
853 @cachedproperty
854 def translations_are_msgids(self):

Subscribers

People subscribed via source and target branches