Merge lp:~henninge/launchpad/devel-487137-custom-language-codes into lp:launchpad

Proposed by Henning Eggers
Status: Merged
Approved by: Henning Eggers
Approved revision: no longer in the source branch.
Merged at revision: 12142
Proposed branch: lp:~henninge/launchpad/devel-487137-custom-language-codes
Merge into: lp:launchpad
Diff against target: 561 lines (+175/-102)
9 files modified
lib/canonical/launchpad/security.py (+28/-0)
lib/lp/translations/browser/configure.zcml (+4/-4)
lib/lp/translations/browser/customlanguagecode.py (+10/-1)
lib/lp/translations/stories/standalone/custom-language-codes.txt (+108/-91)
lib/lp/translations/templates/customlanguagecode-index.pt (+1/-1)
lib/lp/translations/templates/customlanguagecode-remove.pt (+18/-0)
lib/lp/translations/templates/customlanguagecodes-index.pt (+4/-3)
lib/lp/translations/templates/product-portlet-translatables.pt (+1/-1)
lib/lp/translations/templates/sourcepackage-translations.pt (+1/-1)
To merge this branch: bzr merge lp:~henninge/launchpad/devel-487137-custom-language-codes
Reviewer Review Type Date Requested Status
Curtis Hovey (community) ui Approve
Jelmer Vernooij (community) code Approve
Review via email: mp+44446@code.launchpad.net

Commit message

[r=jelmer][ui=sinzui][bug=487137] Let project owners and rosetta admins manage custom language codes. Mainly done by Adi.

Description of the change

= Summary =

This branch finishes off Adi's branch for the same bug which had
already been approved but was never landed. See the details here:
https://code.launchpad.net/~adiroiban/launchpad/bug-487137/+merge/23901

Also, the scope changed slightly because management of custom
language codes has now been extendended to project owners and
distribution translation teams respectively, so they can manage these
themselves. Great way to reduce the work load on admins and Launchapd
devs. ;-)

== Proposed fix ==

Remove AdminCustomLanguageCodes from security.py. This was the main
bug (and a good reason for not landing Adi's branch) as it shadowed
AdminProductTranslations and thus took this privilege away from
product owners. AdminProductTranslations is exactly what we need now
to allow access to owners & Rosetta admins.

Add AdminDistributionSourcePackageTranslations to grant
launchpad.TranslationsAdmin privileges on a DistributionSourcePackage
to the same persons that have it on the distribution.

Fix AdminCustomLanguageCode to allow access for owners and
distribution translation teams.

Update the page test to show the new behavior.

== Pre-implementation notes ==

I read through the previous mp and saw that it was well done. I did
some experiementing to figure out what was wrong with the branch and
found out about the shadowing issue. I talked to Danilo and we
decided to extendend privileges to project owners.

== Implementation details ==

AdminProductTranslations and AdminDistributionTranslations already do
the right thing and are used by the new adapters.
A custom language code can either be linked to a Product or a

DistributionSourcepackage (combination of distribution and
sourcepacakgename), so that the security adapter has to select the
right adapter to forward to.

AdminCustomLanguageCode already implemented forwarding via the
"distribution" attribute, so it could be used, too.

The test was re-written to use the owner_browser for all actions on a
Product. For the DistributionSourcePackage a translation group needed
to be added because its owner will have translation admin privileges
within the distribution. A new translations_browser was added for
this person.

== Tests ==

bin/test -vvcm lp.translations -t custom-language-codes.txt

== Demo and Q/A ==

On launchpad.dev login as <email address hidden> (name12) and go to this
page: https://translations.launchpad.dev/evolution
You should see the "define custom language codes" link.
Click on it and play around with the admin interface.

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/translations/templates/sourcepackage-translations.pt
  lib/lp/translations/templates/customlanguagecode-remove.pt
  lib/lp/translations/browser/configure.zcml
  lib/lp/translations/browser/customlanguagecode.py
  lib/canonical/launchpad/security.py
  lib/lp/translations/templates/customlanguagecodes-index.pt
  lib/lp/translations/templates/customlanguagecode-index.pt
  lib/lp/translations/stories/standalone/custom-language-codes.txt
  lib/lp/translations/templates/product-portlet-translatables.pt

To post a comment you must log in.
Revision history for this message
Henning Eggers (henninge) wrote :

Here is the incremental diff in relation to Adi's branch. This MP is only
about these changes.

1=== modified file 'lib/canonical/launchpad/security.py'
2--- lib/canonical/launchpad/security.py 2010-12-21 15:13:17 +0000
3+++ lib/canonical/launchpad/security.py 2010-12-22 12:03:59 +0000
4@@ -185,7 +185,6 @@
5 from lp.soyuz.interfaces.sourcepackagerelease import ISourcePackageRelease
6 from lp.translations.interfaces.customlanguagecode import (
7 ICustomLanguageCode,
8- IHasCustomLanguageCodes,
9 )
10 from lp.translations.interfaces.languagepack import ILanguagePack
11 from lp.translations.interfaces.pofile import IPOFile
12@@ -744,6 +743,7 @@
13 # with launchpad.View so that this adapter is used. For now, though, it's
14 # going to be used only on the webservice (which explicitly checks for
15 # launchpad.View) so that we don't leak memberships of private teams.
16+
17 class ViewTeamMembership(AuthorizationBase):
18 permission = 'launchpad.View'
19 usedfor = ITeamMembership
20@@ -1728,27 +1728,23 @@
21 usedfor = ILanguage
22
23
24-class AdminCustomLanguageCodes(OnlyRosettaExpertsAndAdmins):
25- """Controls administration of custom language codes.
26-
27- Rosetta experts and Launchpad administrators can administer custom
28- language codes.
29- """
30-
31- permission = 'launchpad.TranslationsAdmin'
32- usedfor = IHasCustomLanguageCodes
33-
34-
35-class AdminCustomLanguageCode(OnlyRosettaExpertsAndAdmins):
36+class AdminCustomLanguageCode(AuthorizationBase):
37 """Controls administration for a custom language code.
38
39- Rosetta experts and Launchpad administrators can administer a custom
40- language code.
41+ Whoever can admin a product's or distribution's translations can also
42+ admin the custom language codes for it.
43 """
44-
45 permission = 'launchpad.TranslationsAdmin'
46 usedfor = ICustomLanguageCode
47
48+ def checkAuthenticated(self, user):
49+ if self.obj.product is not None:
50+ return AdminProductTranslations(
51+ self.obj.product).checkAuthenticated(user)
52+ else:
53+ return AdminDistributionTranslations(
54+ self.obj.distribution).checkAuthenticated(user)
55+
56
57 class AccessBranch(AuthorizationBase):
58 """Controls visibility of branches.
59@@ -1839,6 +1835,12 @@
60 self.obj.distribution).checkAuthenticated(user))
61
62
63+class AdminDistributionSourcePackageTranslations(
64+ AdminDistroSeriesTranslations):
65+ """DistributionSourcePackage objects link to a distribution, too."""
66+ usedfor = IDistributionSourcePackage
67+
68+
69 class AdminProductSeriesTranslations(AuthorizationBase):
70 permission = 'launchpad.TranslationsAdmin'
71 usedfor = IProductSeries
72
73=== modified file 'lib/lp/translations/browser/configure.zcml'
74--- lib/lp/translations/browser/configure.zcml 2010-12-21 15:13:17 +0000
75+++ lib/lp/translations/browser/configure.zcml 2010-12-22 12:01:59 +0000
76@@ -941,7 +941,7 @@
77 for="lp.registry.interfaces.distribution.IDistribution"
78 permission="zope.Public"
79 template="../templates/translations-portlet-not-using-launchpad-extra.pt"
80- layer="lp.translations.publisher.TranslationsLayer"/>
81+ layer="lp.translations.publisher.TranslationsLayer"/>
82 <browser:page
83 name="+portlet-configuration"
84 for="lp.registry.interfaces.distribution.IDistribution"
85
86=== modified file 'lib/lp/translations/browser/customlanguagecode.py'
87--- lib/lp/translations/browser/customlanguagecode.py 2010-12-21 15:13:17 +0000
88+++ lib/lp/translations/browser/customlanguagecode.py 2010-12-22 12:03:05 +0000
89@@ -46,6 +46,7 @@
90
91 class CustomLanguageCodeBreadcrumb(Breadcrumb):
92 """Breadcrumb for a `CustomLanguageCode`."""
93+
94 @property
95 def text(self):
96 return smartquote(
97@@ -164,6 +165,7 @@
98 class HasCustomLanguageCodesTraversalMixin:
99 """Navigate from an `IHasCustomLanguageCodes` to a `CustomLanguageCode`.
100 """
101+
102 @stepthrough('+customcode')
103 def traverseCustomCode(self, name):
104 """Traverse +customcode URLs."""
105
106=== modified file 'lib/lp/translations/stories/standalone/custom-language-codes.txt'
107--- lib/lp/translations/stories/standalone/custom-language-codes.txt 2010-12-21 15:13:17 +0000
108+++ lib/lp/translations/stories/standalone/custom-language-codes.txt 2010-12-22 12:04:22 +0000
109@@ -38,16 +38,18 @@
110 >>> owner_browser = setupBrowser("Basic o@example.com:test")
111 >>> rosetta_admin_browser = setupRosettaExpertBrowser()
112
113-A Launchpad administrator or Rosetta expert sees the link to the custom
114-language codes on a project's main translations page.
115+The project's owner sees the link to the custom language codes on a project's
116+main translations page.
117
118- >>> admin_browser.open(product_page)
119- >>> tag = find_custom_language_codes_link(admin_browser)
120+ >>> owner_browser.open(product_page)
121+ >>> tag = find_custom_language_codes_link(owner_browser)
122 >>> print extract_text(tag.renderContents())
123 If necessary, you may
124 define custom language codes
125 for this project.
126
127+Translation admins also have access to this link.
128+
129 >>> rosetta_admin_browser.open(product_page)
130 >>> tag = find_custom_language_codes_link(rosetta_admin_browser)
131 >>> print extract_text(tag.renderContents())
132@@ -57,39 +59,37 @@
133
134 The link goes to the custom language codes management page.
135
136- >>> rosetta_admin_browser.getLink("define custom language codes").click()
137- >>> custom_language_codes_page = rosetta_admin_browser.url
138-
139-Non-admins, even the project's owner, don't see this link. We do not
140-advertise this feature, since the proper solution is generally to use
141-the right language codes.
142-
143- >>> owner_browser.open(product_page)
144- >>> print find_custom_language_codes_link(owner_browser)
145+ >>> owner_browser.getLink("define custom language codes").click()
146+ >>> custom_language_codes_page = owner_browser.url
147+
148+Other users don't see this link.
149+
150+ >>> user_browser.open(product_page)
151+ >>> print find_custom_language_codes_link(user_browser)
152 None
153
154 Initially the page shows no custom language codes for the project.
155
156- >>> tag = find_tag_by_id(rosetta_admin_browser.contents, 'empty')
157+ >>> tag = find_tag_by_id(owner_browser.contents, 'empty')
158 >>> print extract_text(tag.renderContents())
159 No custom language codes have been defined.
160
161-The admin can add a custom language code.
162-
163- >>> rosetta_admin_browser.getLink("Add a custom language code").click()
164- >>> add_page = rosetta_admin_browser.url
165-
166- >>> rosetta_admin_browser.getControl("Language code:").value = 'no'
167- >>> rosetta_admin_browser.getControl("Language:").value = ['nn']
168- >>> rosetta_admin_browser.getControl("Add").click()
169+There is a link to add a custom language code.
170+
171+ >>> owner_browser.getLink("Add a custom language code").click()
172+ >>> add_page = owner_browser.url
173+
174+ >>> owner_browser.getControl("Language code:").value = 'no'
175+ >>> owner_browser.getControl("Language:").value = ['nn']
176+ >>> owner_browser.getControl("Add").click()
177
178 This leads back to the custom language codes overview, where the new
179 code is now shown.
180
181- >>> rosetta_admin_browser.url == custom_language_codes_page
182+ >>> owner_browser.url == custom_language_codes_page
183 True
184
185- >>> tag = find_tag_by_id(rosetta_admin_browser.contents, 'nonempty')
186+ >>> tag = find_tag_by_id(owner_browser.contents, 'nonempty')
187 >>> print extract_text(tag.renderContents())
188 Foo uses the following custom language codes:
189 Code... ...maps to language
190@@ -98,8 +98,8 @@
191 There is an overview page for the custom code, though there's not much
192 to see there.
193
194- >>> rosetta_admin_browser.getLink("no").click()
195- >>> main = find_main_content(rosetta_admin_browser.contents)
196+ >>> owner_browser.getLink("no").click()
197+ >>> main = find_main_content(owner_browser.contents)
198 >>> print extract_text(main.renderContents())
199 Foo Translations Custom language code ...no...
200 For Foo, uploads with the language code
201@@ -111,79 +111,79 @@
202
203 The overview page leads back to the custom language codes overview.
204
205- >>> code_page = rosetta_admin_browser.url
206- >>> rosetta_admin_browser.getLink(
207+ >>> code_page = owner_browser.url
208+ >>> owner_browser.getLink(
209 ... "custom language codes overview").click()
210- >>> rosetta_admin_browser.url == custom_language_codes_page
211+ >>> owner_browser.url == custom_language_codes_page
212 True
213
214- >>> rosetta_admin_browser.open(code_page)
215+ >>> owner_browser.open(code_page)
216
217-There is also a link for removing codes. The admin follows the link and
218+There is also a link for removing codes. The owner follows the link and
219 removes the "no" custom language code.
220
221- >>> rosetta_admin_browser.getLink("remove custom language code").click()
222- >>> remove_page = rosetta_admin_browser.url
223- >>> rosetta_admin_browser.getControl("Remove").click()
224+ >>> owner_browser.getLink("remove custom language code").click()
225+ >>> remove_page = owner_browser.url
226+ >>> owner_browser.getControl("Remove").click()
227
228 This leads back to the overview page.
229
230- >>> rosetta_admin_browser.url == custom_language_codes_page
231+ >>> owner_browser.url == custom_language_codes_page
232 True
233
234- >>> tag = find_tag_by_id(rosetta_admin_browser.contents, 'empty')
235- >>> print extract_text(tag.renderContents())
236- No custom language codes have been defined.
237-
238-
239-Non-admin access
240-================
241-
242-A non-admin can see the page, actually, if they know the URL. This can
243-be convenient for debugging.
244-
245- >>> owner_browser.open(custom_language_codes_page)
246-
247 >>> tag = find_tag_by_id(owner_browser.contents, 'empty')
248 >>> print extract_text(tag.renderContents())
249 No custom language codes have been defined.
250
251+
252+Unprivileged access
253+===================
254+
255+A unprivileged user can see the page, actually, if they know the URL.
256+This can be convenient for debugging.
257+
258+ >>> user_browser.open(custom_language_codes_page)
259+
260+ >>> tag = find_tag_by_id(user_browser.contents, 'empty')
261+ >>> print extract_text(tag.renderContents())
262+ No custom language codes have been defined.
263+
264 However all they get is a read-only version of the page.
265
266- >>> owner_browser.getLink("Add a custom language code").click()
267+ >>> user_browser.getLink("Add a custom language code").click()
268 Traceback (most recent call last):
269 ...
270 LinkNotFoundError
271
272 The page for adding custom language codes is not accessible to them.
273
274+ >>> user_browser.open(add_page)
275+ Traceback (most recent call last):
276+ ...
277+ Unauthorized: ...
278+
279+And naturally, if the owner creates a custom language code again, an
280+unprivileged user can't remove it.
281+
282 >>> owner_browser.open(add_page)
283- Traceback (most recent call last):
284- ...
285- Unauthorized: ...
286-
287-And naturally, if an admin creates a custom language code again, a
288-non-admin can't remove it.
289-
290- >>> rosetta_admin_browser.open(add_page)
291- >>> rosetta_admin_browser.getControl("Language code:").value = 'no'
292- >>> rosetta_admin_browser.getControl("Language:").value = ['nn']
293- >>> rosetta_admin_browser.getControl("Add").click()
294-
295- >>> owner_browser.open(custom_language_codes_page)
296- >>> tag = find_tag_by_id(owner_browser.contents, 'nonempty')
297+ >>> owner_browser.getControl("Language code:").value = 'no'
298+ >>> owner_browser.getControl("Language:").value = ['nn']
299+ >>> owner_browser.getControl("Add").click()
300+
301+ >>> user_browser.open(custom_language_codes_page)
302+ >>> tag = find_tag_by_id(user_browser.contents, 'nonempty')
303 >>> print extract_text(tag.renderContents())
304 Foo uses the following custom language codes:
305 Code... ...maps to language
306 no Norwegian Nynorsk
307
308- >>> owner_browser.getLink("no").click()
309- >>> owner_browser.getLink("remove custom language code")
310+ >>> user_browser.getLink("no").click()
311+ >>> user_browser.getLink("remove custom language code")
312 Traceback (most recent call last):
313 ...
314 LinkNotFoundError
315
316- >>> owner_browser.open(remove_page)
317+ >>> user_browser.open(remove_page)
318 Traceback (most recent call last):
319 ...
320 Unauthorized: ...
321@@ -197,7 +197,8 @@
322 package--i.e. the combination of a distribution and a source package
323 name. However, since there is no Translations page for that type of
324 object (and we'd probably never go there if there were), the link is
325-shown on the source package page.
326+shown on the source package page. For distributions, the owner of the
327+distribution's translation group is a translations administrator.
328
329 >>> login(ANONYMOUS)
330 >>> from lp.registry.model.sourcepackage import SourcePackage
331@@ -219,38 +220,44 @@
332 ... distroseries=other_series,
333 ... sourcepackagename=package.sourcepackagename),
334 ... rootsite="translations")
335+ >>> translations_admin = factory.makePerson(
336+ ... email='ta@example.com', password='test')
337+ >>> translationgroup = factory.makeTranslationGroup(
338+ ... owner=translations_admin)
339+ >>> removeSecurityProxy(distro).translationgroup = translationgroup
340 >>> logout()
341
342- >>> rosetta_admin_browser.open(package_page)
343+ >>> translations_browser = setupBrowser("Basic ta@example.com:test")
344+ >>> translations_browser.open(package_page)
345
346 Of course in this case, the notice about there being no custom language
347 codes talks about a package, not a project.
348
349- >>> tag = find_custom_language_codes_link(rosetta_admin_browser)
350+ >>> tag = find_custom_language_codes_link(translations_browser)
351 >>> print extract_text(tag.renderContents())
352 If necessary, you may
353 define custom language codes
354 for this package.
355
356- >>> rosetta_admin_browser.getLink("define custom language codes").click()
357- >>> custom_language_codes_page = rosetta_admin_browser.url
358+ >>> translations_browser.getLink("define custom language codes").click()
359+ >>> custom_language_codes_page = translations_browser.url
360
361- >>> tag = find_tag_by_id(rosetta_admin_browser.contents, 'empty')
362+ >>> tag = find_tag_by_id(translations_browser.contents, 'empty')
363 >>> print extract_text(tag.renderContents())
364 No custom language codes have been defined.
365
366-Again, an admin can add a language code.
367-
368- >>> rosetta_admin_browser.getLink("Add a custom language code").click()
369- >>> add_page = rosetta_admin_browser.url
370-
371- >>> rosetta_admin_browser.getControl("Language code:").value = 'pt-br'
372- >>> rosetta_admin_browser.getControl("Language:").value = ['pt_BR']
373- >>> rosetta_admin_browser.getControl("Add").click()
374+A translations admin can add a language code.
375+
376+ >>> translations_browser.getLink("Add a custom language code").click()
377+ >>> add_page = translations_browser.url
378+
379+ >>> translations_browser.getControl("Language code:").value = 'pt-br'
380+ >>> translations_browser.getControl("Language:").value = ['pt_BR']
381+ >>> translations_browser.getControl("Add").click()
382
383 The language code is displayed.
384
385- >>> tag = find_tag_by_id(rosetta_admin_browser.contents, 'nonempty')
386+ >>> tag = find_tag_by_id(translations_browser.contents, 'nonempty')
387 >>> print extract_text(tag.renderContents())
388 bar in distro uses the following custom language codes:
389 Code... ...maps to language
390@@ -259,15 +266,15 @@
391 It's also displayed identically on the same package but in another
392 release series of the same distribution.
393
394- >>> rosetta_admin_browser.open(page_in_other_series)
395- >>> tag = find_custom_language_codes_link(rosetta_admin_browser)
396+ >>> translations_browser.open(page_in_other_series)
397+ >>> tag = find_custom_language_codes_link(translations_browser)
398 >>> print extract_text(tag.renderContents())
399 If necessary, you may
400 define custom language codes
401 for this package.
402
403- >>> rosetta_admin_browser.getLink("define custom language codes").click()
404- >>> tag = find_tag_by_id(rosetta_admin_browser.contents, 'nonempty')
405+ >>> translations_browser.getLink("define custom language codes").click()
406+ >>> tag = find_tag_by_id(translations_browser.contents, 'nonempty')
407 >>> print extract_text(tag.renderContents())
408 bar in distro uses the following custom language codes:
409 Code... ...maps to language
410@@ -276,13 +283,13 @@
411
412 The new code has a link there...
413
414- >>> rosetta_admin_browser.getLink("pt-br").click()
415+ >>> translations_browser.getLink("pt-br").click()
416
417 ...and can be deleted.
418
419- >>> rosetta_admin_browser.getLink("remove custom language code").click()
420- >>> rosetta_admin_browser.getControl("Remove").click()
421+ >>> translations_browser.getLink("remove custom language code").click()
422+ >>> translations_browser.getControl("Remove").click()
423
424- >>> tag = find_tag_by_id(rosetta_admin_browser.contents, 'empty')
425+ >>> tag = find_tag_by_id(translations_browser.contents, 'empty')
426 >>> print extract_text(tag.renderContents())
427 No custom language codes have been defined.
Revision history for this message
Jelmer Vernooij (jelmer) :
review: Approve (code)
Revision history for this message
Henning Eggers (henninge) wrote :

I just realized that this branch needs a UI review, too. Here is a walk-through with some screen shots.

Starting on a product's translation page as the products owner. You see the link to "define custom languge codes".
http://people.canonical.com/~henninge/screenshots/custom_language_codes_1_product.png

Clicking that link will bring you to the listing of custom language codes for this product which is still empty.
http://people.canonical.com/~henninge/screenshots/custom_language_codes_2_index_empty.png

Clicking "Add a custom language code" will bring you to the add form.
http://people.canonical.com/~henninge/screenshots/custom_language_codes_3_add.png

Now the listing has one entry.
http://people.canonical.com/~henninge/screenshots/custom_language_codes_4_index_one.png

Clicking on the custom code brings up the details page for this code.
http://people.canonical.com/~henninge/screenshots/custom_language_codes_5_one.png

And trying to remove it will bring up the removal page.
http://people.canonical.com/~henninge/screenshots/custom_language_codes_6_remove.png

Almost could have done a screencast ... ;)

Revision history for this message
Curtis Hovey (sinzui) wrote :

Hi Henning.

This looks good. I have a question. Why is custom_language_codes_5_one.png missing a header? Maybe Custom language code 'qq' for Evolution?

review: Needs Information (ui)
Revision history for this message
Henning Eggers (henninge) wrote :

Good catch, thank you! Here is a screenshot of the fixed view.

http://people.canonical.com/~henninge/screenshots/custom_language_codes_5a_one.png

Revision history for this message
Curtis Hovey (sinzui) wrote :

I think this is good to land.

review: Approve (ui)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/canonical/launchpad/security.py'
2--- lib/canonical/launchpad/security.py 2010-12-17 20:28:40 +0000
3+++ lib/canonical/launchpad/security.py 2010-12-23 09:44:44 +0000
4@@ -183,6 +183,9 @@
5 IPackageUploadQueue,
6 )
7 from lp.soyuz.interfaces.sourcepackagerelease import ISourcePackageRelease
8+from lp.translations.interfaces.customlanguagecode import (
9+ ICustomLanguageCode,
10+ )
11 from lp.translations.interfaces.languagepack import ILanguagePack
12 from lp.translations.interfaces.pofile import IPOFile
13 from lp.translations.interfaces.potemplate import IPOTemplate
14@@ -740,6 +743,7 @@
15 # with launchpad.View so that this adapter is used. For now, though, it's
16 # going to be used only on the webservice (which explicitly checks for
17 # launchpad.View) so that we don't leak memberships of private teams.
18+
19 class ViewTeamMembership(AuthorizationBase):
20 permission = 'launchpad.View'
21 usedfor = ITeamMembership
22@@ -1724,6 +1728,24 @@
23 usedfor = ILanguage
24
25
26+class AdminCustomLanguageCode(AuthorizationBase):
27+ """Controls administration for a custom language code.
28+
29+ Whoever can admin a product's or distribution's translations can also
30+ admin the custom language codes for it.
31+ """
32+ permission = 'launchpad.TranslationsAdmin'
33+ usedfor = ICustomLanguageCode
34+
35+ def checkAuthenticated(self, user):
36+ if self.obj.product is not None:
37+ return AdminProductTranslations(
38+ self.obj.product).checkAuthenticated(user)
39+ else:
40+ return AdminDistributionTranslations(
41+ self.obj.distribution).checkAuthenticated(user)
42+
43+
44 class AccessBranch(AuthorizationBase):
45 """Controls visibility of branches.
46
47@@ -1813,6 +1835,12 @@
48 self.obj.distribution).checkAuthenticated(user))
49
50
51+class AdminDistributionSourcePackageTranslations(
52+ AdminDistroSeriesTranslations):
53+ """DistributionSourcePackage objects link to a distribution, too."""
54+ usedfor = IDistributionSourcePackage
55+
56+
57 class AdminProductSeriesTranslations(AuthorizationBase):
58 permission = 'launchpad.TranslationsAdmin'
59 usedfor = IProductSeries
60
61=== modified file 'lib/lp/translations/browser/configure.zcml'
62--- lib/lp/translations/browser/configure.zcml 2010-12-01 11:26:57 +0000
63+++ lib/lp/translations/browser/configure.zcml 2010-12-23 09:44:44 +0000
64@@ -941,7 +941,7 @@
65 for="lp.registry.interfaces.distribution.IDistribution"
66 permission="zope.Public"
67 template="../templates/translations-portlet-not-using-launchpad-extra.pt"
68- layer="lp.translations.publisher.TranslationsLayer"/>
69+ layer="lp.translations.publisher.TranslationsLayer"/>
70 <browser:page
71 name="+portlet-configuration"
72 for="lp.registry.interfaces.distribution.IDistribution"
73@@ -1042,9 +1042,9 @@
74 <browser:page
75 name="+remove"
76 for="lp.translations.interfaces.customlanguagecode.ICustomLanguageCode"
77- permission="launchpad.Admin"
78+ permission="launchpad.TranslationsAdmin"
79 class="lp.translations.browser.customlanguagecode.CustomLanguageCodeRemoveView"
80- template="../../app/templates/generic-edit.pt"
81+ template="../templates/customlanguagecode-remove.pt"
82 layer="lp.translations.publisher.TranslationsLayer"/>
83
84 <!-- IHasCustomLanguageCodes -->
85@@ -1062,7 +1062,7 @@
86 layer="lp.translations.publisher.TranslationsLayer"
87 class="lp.translations.browser.customlanguagecode.CustomLanguageCodeAddView"
88 template="../templates/customlanguagecode-add.pt"
89- permission="launchpad.Admin"/>
90+ permission="launchpad.TranslationsAdmin"/>
91
92 </facet>
93
94
95=== modified file 'lib/lp/translations/browser/customlanguagecode.py'
96--- lib/lp/translations/browser/customlanguagecode.py 2010-12-01 11:26:57 +0000
97+++ lib/lp/translations/browser/customlanguagecode.py 2010-12-23 09:44:44 +0000
98@@ -11,7 +11,7 @@
99 'CustomLanguageCodeView',
100 'HasCustomLanguageCodesNavigation',
101 'HasCustomLanguageCodesTraversalMixin',
102- ]
103+ ]
104
105
106 import re
107@@ -46,6 +46,7 @@
108
109 class CustomLanguageCodeBreadcrumb(Breadcrumb):
110 """Breadcrumb for a `CustomLanguageCode`."""
111+
112 @property
113 def text(self):
114 return smartquote(
115@@ -126,6 +127,13 @@
116 class CustomLanguageCodeView(LaunchpadView):
117 schema = ICustomLanguageCode
118
119+ @property
120+ def label(self):
121+ target_displayname = self.context.translation_target.displayname
122+ return smartquote(
123+ 'Custom language code "%s" for %s' % (
124+ self.context.language_code, target_displayname))
125+
126
127 class CustomLanguageCodeRemoveView(LaunchpadFormView):
128 """View for removing a `CustomLanguageCode`."""
129@@ -164,6 +172,7 @@
130 class HasCustomLanguageCodesTraversalMixin:
131 """Navigate from an `IHasCustomLanguageCodes` to a `CustomLanguageCode`.
132 """
133+
134 @stepthrough('+customcode')
135 def traverseCustomCode(self, name):
136 """Traverse +customcode URLs."""
137
138=== modified file 'lib/lp/translations/stories/standalone/custom-language-codes.txt'
139--- lib/lp/translations/stories/standalone/custom-language-codes.txt 2010-08-31 23:03:45 +0000
140+++ lib/lp/translations/stories/standalone/custom-language-codes.txt 2010-12-23 09:44:44 +0000
141@@ -36,12 +36,22 @@
142 >>> logout()
143
144 >>> owner_browser = setupBrowser("Basic o@example.com:test")
145-
146-An administrator sees the link to the custom language codes on a
147-project's main translations page.
148-
149- >>> admin_browser.open(product_page)
150- >>> tag = find_custom_language_codes_link(admin_browser)
151+ >>> rosetta_admin_browser = setupRosettaExpertBrowser()
152+
153+The project's owner sees the link to the custom language codes on a project's
154+main translations page.
155+
156+ >>> owner_browser.open(product_page)
157+ >>> tag = find_custom_language_codes_link(owner_browser)
158+ >>> print extract_text(tag.renderContents())
159+ If necessary, you may
160+ define custom language codes
161+ for this project.
162+
163+Translation admins also have access to this link.
164+
165+ >>> rosetta_admin_browser.open(product_page)
166+ >>> tag = find_custom_language_codes_link(rosetta_admin_browser)
167 >>> print extract_text(tag.renderContents())
168 If necessary, you may
169 define custom language codes
170@@ -49,39 +59,37 @@
171
172 The link goes to the custom language codes management page.
173
174- >>> admin_browser.getLink("define custom language codes").click()
175- >>> custom_language_codes_page = admin_browser.url
176-
177-Non-admins, even the project's owner, don't see this link. We do not
178-advertise this feature, since the proper solution is generally to use
179-the right language codes.
180-
181- >>> owner_browser.open(product_page)
182- >>> print find_custom_language_codes_link(owner_browser)
183+ >>> owner_browser.getLink("define custom language codes").click()
184+ >>> custom_language_codes_page = owner_browser.url
185+
186+Other users don't see this link.
187+
188+ >>> user_browser.open(product_page)
189+ >>> print find_custom_language_codes_link(user_browser)
190 None
191
192 Initially the page shows no custom language codes for the project.
193
194- >>> tag = find_tag_by_id(admin_browser.contents, 'empty')
195+ >>> tag = find_tag_by_id(owner_browser.contents, 'empty')
196 >>> print extract_text(tag.renderContents())
197 No custom language codes have been defined.
198
199-The admin can add a custom language code.
200-
201- >>> admin_browser.getLink("Add a custom language code").click()
202- >>> add_page = admin_browser.url
203-
204- >>> admin_browser.getControl("Language code:").value = 'no'
205- >>> admin_browser.getControl("Language:").value = ['nn']
206- >>> admin_browser.getControl("Add").click()
207+There is a link to add a custom language code.
208+
209+ >>> owner_browser.getLink("Add a custom language code").click()
210+ >>> add_page = owner_browser.url
211+
212+ >>> owner_browser.getControl("Language code:").value = 'no'
213+ >>> owner_browser.getControl("Language:").value = ['nn']
214+ >>> owner_browser.getControl("Add").click()
215
216 This leads back to the custom language codes overview, where the new
217 code is now shown.
218
219- >>> admin_browser.url == custom_language_codes_page
220+ >>> owner_browser.url == custom_language_codes_page
221 True
222
223- >>> tag = find_tag_by_id(admin_browser.contents, 'nonempty')
224+ >>> tag = find_tag_by_id(owner_browser.contents, 'nonempty')
225 >>> print extract_text(tag.renderContents())
226 Foo uses the following custom language codes:
227 Code... ...maps to language
228@@ -90,9 +98,10 @@
229 There is an overview page for the custom code, though there's not much
230 to see there.
231
232- >>> admin_browser.getLink("no").click()
233- >>> main = find_main_content(admin_browser.contents)
234+ >>> owner_browser.getLink("no").click()
235+ >>> main = find_main_content(owner_browser.contents)
236 >>> print extract_text(main.renderContents())
237+ Custom language code ...no... for Foo
238 Foo Translations Custom language code ...no...
239 For Foo, uploads with the language code
240 &ldquo;no&rdquo;
241@@ -103,78 +112,79 @@
242
243 The overview page leads back to the custom language codes overview.
244
245- >>> code_page = admin_browser.url
246- >>> admin_browser.getLink("custom language codes overview").click()
247- >>> admin_browser.url == custom_language_codes_page
248+ >>> code_page = owner_browser.url
249+ >>> owner_browser.getLink(
250+ ... "custom language codes overview").click()
251+ >>> owner_browser.url == custom_language_codes_page
252 True
253
254- >>> admin_browser.open(code_page)
255+ >>> owner_browser.open(code_page)
256
257-There is also a link for removing codes. The admin follows the link and
258+There is also a link for removing codes. The owner follows the link and
259 removes the "no" custom language code.
260
261- >>> admin_browser.getLink("remove custom language code").click()
262- >>> remove_page = admin_browser.url
263- >>> admin_browser.getControl("Remove").click()
264+ >>> owner_browser.getLink("remove custom language code").click()
265+ >>> remove_page = owner_browser.url
266+ >>> owner_browser.getControl("Remove").click()
267
268 This leads back to the overview page.
269
270- >>> admin_browser.url == custom_language_codes_page
271+ >>> owner_browser.url == custom_language_codes_page
272 True
273
274- >>> tag = find_tag_by_id(admin_browser.contents, 'empty')
275- >>> print extract_text(tag.renderContents())
276- No custom language codes have been defined.
277-
278-
279-Non-admin access
280-================
281-
282-A non-admin can see the page, actually, if they know the URL. This can
283-be convenient for debugging.
284-
285- >>> owner_browser.open(custom_language_codes_page)
286-
287 >>> tag = find_tag_by_id(owner_browser.contents, 'empty')
288 >>> print extract_text(tag.renderContents())
289 No custom language codes have been defined.
290
291+
292+Unprivileged access
293+===================
294+
295+A unprivileged user can see the page, actually, if they know the URL.
296+This can be convenient for debugging.
297+
298+ >>> user_browser.open(custom_language_codes_page)
299+
300+ >>> tag = find_tag_by_id(user_browser.contents, 'empty')
301+ >>> print extract_text(tag.renderContents())
302+ No custom language codes have been defined.
303+
304 However all they get is a read-only version of the page.
305
306- >>> owner_browser.getLink("Add a custom language code").click()
307+ >>> user_browser.getLink("Add a custom language code").click()
308 Traceback (most recent call last):
309 ...
310 LinkNotFoundError
311
312 The page for adding custom language codes is not accessible to them.
313
314+ >>> user_browser.open(add_page)
315+ Traceback (most recent call last):
316+ ...
317+ Unauthorized: ...
318+
319+And naturally, if the owner creates a custom language code again, an
320+unprivileged user can't remove it.
321+
322 >>> owner_browser.open(add_page)
323- Traceback (most recent call last):
324- ...
325- Unauthorized: ...
326-
327-And naturally, if an admin creates a custom language code again, a
328-non-admin can't remove it.
329-
330- >>> admin_browser.open(add_page)
331- >>> admin_browser.getControl("Language code:").value = 'no'
332- >>> admin_browser.getControl("Language:").value = ['nn']
333- >>> admin_browser.getControl("Add").click()
334-
335- >>> owner_browser.open(custom_language_codes_page)
336- >>> tag = find_tag_by_id(owner_browser.contents, 'nonempty')
337+ >>> owner_browser.getControl("Language code:").value = 'no'
338+ >>> owner_browser.getControl("Language:").value = ['nn']
339+ >>> owner_browser.getControl("Add").click()
340+
341+ >>> user_browser.open(custom_language_codes_page)
342+ >>> tag = find_tag_by_id(user_browser.contents, 'nonempty')
343 >>> print extract_text(tag.renderContents())
344 Foo uses the following custom language codes:
345 Code... ...maps to language
346 no Norwegian Nynorsk
347
348- >>> owner_browser.getLink("no").click()
349- >>> owner_browser.getLink("remove custom language code")
350+ >>> user_browser.getLink("no").click()
351+ >>> user_browser.getLink("remove custom language code")
352 Traceback (most recent call last):
353 ...
354 LinkNotFoundError
355
356- >>> owner_browser.open(remove_page)
357+ >>> user_browser.open(remove_page)
358 Traceback (most recent call last):
359 ...
360 Unauthorized: ...
361@@ -188,7 +198,8 @@
362 package--i.e. the combination of a distribution and a source package
363 name. However, since there is no Translations page for that type of
364 object (and we'd probably never go there if there were), the link is
365-shown on the source package page.
366+shown on the source package page. For distributions, the owner of the
367+distribution's translation group is a translations administrator.
368
369 >>> login(ANONYMOUS)
370 >>> from lp.registry.model.sourcepackage import SourcePackage
371@@ -210,38 +221,44 @@
372 ... distroseries=other_series,
373 ... sourcepackagename=package.sourcepackagename),
374 ... rootsite="translations")
375+ >>> translations_admin = factory.makePerson(
376+ ... email='ta@example.com', password='test')
377+ >>> translationgroup = factory.makeTranslationGroup(
378+ ... owner=translations_admin)
379+ >>> removeSecurityProxy(distro).translationgroup = translationgroup
380 >>> logout()
381
382- >>> admin_browser.open(package_page)
383+ >>> translations_browser = setupBrowser("Basic ta@example.com:test")
384+ >>> translations_browser.open(package_page)
385
386 Of course in this case, the notice about there being no custom language
387 codes talks about a package, not a project.
388
389- >>> tag = find_custom_language_codes_link(admin_browser)
390+ >>> tag = find_custom_language_codes_link(translations_browser)
391 >>> print extract_text(tag.renderContents())
392 If necessary, you may
393 define custom language codes
394 for this package.
395
396- >>> admin_browser.getLink("define custom language codes").click()
397- >>> custom_language_codes_page = admin_browser.url
398+ >>> translations_browser.getLink("define custom language codes").click()
399+ >>> custom_language_codes_page = translations_browser.url
400
401- >>> tag = find_tag_by_id(admin_browser.contents, 'empty')
402+ >>> tag = find_tag_by_id(translations_browser.contents, 'empty')
403 >>> print extract_text(tag.renderContents())
404 No custom language codes have been defined.
405
406-Again, an admin can add a language code.
407-
408- >>> admin_browser.getLink("Add a custom language code").click()
409- >>> add_page = admin_browser.url
410-
411- >>> admin_browser.getControl("Language code:").value = 'pt-br'
412- >>> admin_browser.getControl("Language:").value = ['pt_BR']
413- >>> admin_browser.getControl("Add").click()
414+A translations admin can add a language code.
415+
416+ >>> translations_browser.getLink("Add a custom language code").click()
417+ >>> add_page = translations_browser.url
418+
419+ >>> translations_browser.getControl("Language code:").value = 'pt-br'
420+ >>> translations_browser.getControl("Language:").value = ['pt_BR']
421+ >>> translations_browser.getControl("Add").click()
422
423 The language code is displayed.
424
425- >>> tag = find_tag_by_id(admin_browser.contents, 'nonempty')
426+ >>> tag = find_tag_by_id(translations_browser.contents, 'nonempty')
427 >>> print extract_text(tag.renderContents())
428 bar in distro uses the following custom language codes:
429 Code... ...maps to language
430@@ -250,15 +267,15 @@
431 It's also displayed identically on the same package but in another
432 release series of the same distribution.
433
434- >>> admin_browser.open(page_in_other_series)
435- >>> tag = find_custom_language_codes_link(admin_browser)
436+ >>> translations_browser.open(page_in_other_series)
437+ >>> tag = find_custom_language_codes_link(translations_browser)
438 >>> print extract_text(tag.renderContents())
439 If necessary, you may
440 define custom language codes
441 for this package.
442
443- >>> admin_browser.getLink("define custom language codes").click()
444- >>> tag = find_tag_by_id(admin_browser.contents, 'nonempty')
445+ >>> translations_browser.getLink("define custom language codes").click()
446+ >>> tag = find_tag_by_id(translations_browser.contents, 'nonempty')
447 >>> print extract_text(tag.renderContents())
448 bar in distro uses the following custom language codes:
449 Code... ...maps to language
450@@ -267,13 +284,13 @@
451
452 The new code has a link there...
453
454- >>> admin_browser.getLink("pt-br").click()
455+ >>> translations_browser.getLink("pt-br").click()
456
457 ...and can be deleted.
458
459- >>> admin_browser.getLink("remove custom language code").click()
460- >>> admin_browser.getControl("Remove").click()
461+ >>> translations_browser.getLink("remove custom language code").click()
462+ >>> translations_browser.getControl("Remove").click()
463
464- >>> tag = find_tag_by_id(admin_browser.contents, 'empty')
465+ >>> tag = find_tag_by_id(translations_browser.contents, 'empty')
466 >>> print extract_text(tag.renderContents())
467 No custom language codes have been defined.
468
469=== modified file 'lib/lp/translations/templates/customlanguagecode-index.pt'
470--- lib/lp/translations/templates/customlanguagecode-index.pt 2010-08-20 01:41:58 +0000
471+++ lib/lp/translations/templates/customlanguagecode-index.pt 2010-12-23 09:44:44 +0000
472@@ -26,7 +26,7 @@
473
474 <div class="portlet">
475 <ul class="horizontal">
476- <li tal:condition="context/required:launchpad.Admin">
477+ <li tal:condition="context/required:launchpad.TranslationsAdmin">
478 <a class="remove sprite"
479 tal:attributes="href context/fmt:url:translations/+remove">
480 remove custom language code
481
482=== added file 'lib/lp/translations/templates/customlanguagecode-remove.pt'
483--- lib/lp/translations/templates/customlanguagecode-remove.pt 1970-01-01 00:00:00 +0000
484+++ lib/lp/translations/templates/customlanguagecode-remove.pt 2010-12-23 09:44:44 +0000
485@@ -0,0 +1,18 @@
486+<html
487+ xmlns="http://www.w3.org/1999/xhtml"
488+ xmlns:tal="http://xml.zope.org/namespaces/tal"
489+ xmlns:metal="http://xml.zope.org/namespaces/metal"
490+ xmlns:i18n="http://xml.zope.org/namespaces/i18n"
491+ metal:use-macro="view/macro:page/main_only"
492+ i18n:domain="launchpad">
493+ <body>
494+ <div metal:fill-slot="main">
495+ <div metal:use-macro="context/@@launchpad_form/form">
496+ <div metal:fill-slot="extra_info" class="documentDescription">
497+ You are going to remove the custom language code
498+ '<tal:language-code replace="view/code" />'.
499+ </div>
500+ </div>
501+ </div>
502+ </body>
503+</html>
504
505=== modified file 'lib/lp/translations/templates/customlanguagecodes-index.pt'
506--- lib/lp/translations/templates/customlanguagecodes-index.pt 2010-08-20 01:41:58 +0000
507+++ lib/lp/translations/templates/customlanguagecodes-index.pt 2010-12-23 09:44:44 +0000
508@@ -33,7 +33,8 @@
509 <tr>
510 <th>Code...</th>
511 <th>...maps to language</th>
512- <th tal:condition="context/required:launchpad.Admin"></th>
513+ <th tal:condition="context/required:launchpad.TranslationsAdmin">
514+ </th>
515 </tr>
516 </thead>
517 <tbody>
518@@ -51,7 +52,7 @@
519 &mdash;
520 </tal:nolanguage>
521 </td>
522- <td tal:condition="context/required:launchpad.Admin">
523+ <td tal:condition="context/required:launchpad.TranslationsAdmin">
524 <a tal:attributes="href entry/fmt:url:translations/+remove"
525 alt="Remove"
526 title="Remove"
527@@ -70,7 +71,7 @@
528
529 <div>
530 <a tal:attributes="href context/fmt:url:translations/+add-custom-language-code"
531- tal:condition="context/required:launchpad.Admin"
532+ tal:condition="context/required:launchpad.TranslationsAdmin"
533 class="add sprite">
534 Add a custom language code
535 </a>
536
537=== modified file 'lib/lp/translations/templates/product-portlet-translatables.pt'
538--- lib/lp/translations/templates/product-portlet-translatables.pt 2010-08-20 00:39:54 +0000
539+++ lib/lp/translations/templates/product-portlet-translatables.pt 2010-12-23 09:44:44 +0000
540@@ -65,7 +65,7 @@
541 </div>
542
543 <div class="portlet"
544- tal:condition="context/required:launchpad.Admin"
545+ tal:condition="context/required:launchpad.TranslationsAdmin"
546 id="custom-language-codes">
547 If necessary, you may
548 <a tal:attributes="href context/fmt:url:translations/+custom-language-codes"
549
550=== modified file 'lib/lp/translations/templates/sourcepackage-translations.pt'
551--- lib/lp/translations/templates/sourcepackage-translations.pt 2010-10-10 21:54:16 +0000
552+++ lib/lp/translations/templates/sourcepackage-translations.pt 2010-12-23 09:44:44 +0000
553@@ -39,7 +39,7 @@
554 <a tal:attributes="href context/menu:navigation/download/url">
555 download a full tarball</a> with translations.
556 </p>
557- <p tal:condition="context/required:launchpad.Admin"
558+ <p tal:condition="context/distribution_sourcepackage/required:launchpad.TranslationsAdmin"
559 id="custom-language-codes">
560 If necessary, you may
561 <a tal:attributes="href context/distribution_sourcepackage/fmt:url:translations/+custom-language-codes"