Merge lp:~jtv/launchpad/custom-language-codes into lp:launchpad

Proposed by Jeroen T. Vermeulen
Status: Merged
Approved by: Abel Deuring
Approved revision: not available
Merged at revision: not available
Proposed branch: lp:~jtv/launchpad/custom-language-codes
Merge into: lp:launchpad
Diff against target: 1255 lines (+878/-67)
21 files modified
lib/lp/registry/browser/distributionsourcepackage.py (+4/-1)
lib/lp/registry/browser/product.py (+4/-2)
lib/lp/registry/configure.zcml (+5/-0)
lib/lp/registry/interfaces/distribution.py (+0/-7)
lib/lp/registry/interfaces/product.py (+0/-7)
lib/lp/registry/model/distribution.py (+0/-7)
lib/lp/registry/model/distributionsourcepackage.py (+23/-2)
lib/lp/registry/model/product.py (+15/-9)
lib/lp/translations/browser/configure.zcml (+45/-0)
lib/lp/translations/browser/customlanguagecode.py (+171/-0)
lib/lp/translations/configure.zcml (+5/-0)
lib/lp/translations/interfaces/customlanguagecode.py (+65/-10)
lib/lp/translations/model/customlanguagecode.py (+72/-3)
lib/lp/translations/model/translationimportqueue.py (+5/-4)
lib/lp/translations/stories/standalone/custom-language-codes.txt (+276/-0)
lib/lp/translations/templates/customlanguagecode-add.pt (+29/-0)
lib/lp/translations/templates/customlanguagecode-index.pt (+46/-0)
lib/lp/translations/templates/customlanguagecodes-index.pt (+80/-0)
lib/lp/translations/templates/product-portlet-translatables.pt (+12/-0)
lib/lp/translations/templates/sourcepackage-translations.pt (+9/-0)
lib/lp/translations/tests/test_autoapproval.py (+12/-15)
To merge this branch: bzr merge lp:~jtv/launchpad/custom-language-codes
Reviewer Review Type Date Requested Status
Abel Deuring (community) code Approve
Martin Albisetti ui Pending
Review via email: mp+14555@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :
Download full text (3.3 KiB)

= Bug 271747: Custom Language Codes UI =

This is an oversized branch, and it's not particularly urgent. Review it only if you feel like it, or something's changed and we've begged you to do it.

Some projects or Ubuntu packages insist on using nonstandard language codes. To deal with those we created, long ago, "custom language codes" which allow the import approval process to recognize that an upload with a given "weird" language code is actually, by the project's or package's standard, a translation for a given language.

We don't advertise this feature, and it's not frequently used. Project owners do not get access to it. It's really only there for a few problem cases like OpenOffice.org, which not only use nonstandard language codes but also add or move templates regularly. If it were just a matter of a single translation, you could simply approve it once and the approver would remember the path and associate it with the right translation.

However, it is a pain in the neck to have to write SQL, get approval, and go through LOSAs every time we want one of these babies added. So this branch finally creates a simple UI for dealing with them.

The simple UI lists custom language codes, shows them in detail, adds them, and removes them. For non-admins there's a read-only view of the whole thing, which may come in handy when debugging, but we don't link to them because generally speaking, custom language codes are probably the wrong solution for a project having problem with its language codes. And it adds one more knob to twiddle, providing one more thing that may go wrong with import approvals.

One limitation: this branch allows only Launchpad admins to manipulate custom language codes. In future we'll also want to allow Rosetta admins to do this. But that required some fiddling to check for the right permissions. This branch is already oversized, so instead I intend to file a separate bug about it.

== Tests ==
{{{
./bin/test -vv -t custom-language-codes.txt
}}}

== Demo, Q/A, and UI review ==

Log in as a Launchpad administrator. Go to the Translations page for a project or source package with translations.

On staging:
    https://translations.staging.launchpad.net/josm/

On a development machine:
    https://translations.launchpad.dev/evolution/

At the bottom of the right-hand side column you'll see a note: "If necessary, you may <define custom language codes> for this project." Click on the link to get to the custom language codes UI for the project.

The page it takes you to is an overview, probably of nothing as yet. Try adding a custom language code: map a custom "language code" to an actual language, or to no language at all. (If you map to no language, uploads with that language code string will quietly disappear—useful but one of those things that can cause support headaches if misused). "Weird" characters are rejected; we don't want to be able to redefine the ".po" filename extension to refer to a language or anything like that.

Whatever you add will show up in the listing. You can also remove or view the codes. Viewing them doesn't give you much, besides a link to the language, but it was needed to make t...

Read more...

Revision history for this message
Abel Deuring (adeuring) wrote :

Hi Jeroen,

a nice branch, the Losas will appreciate it, I am sure.

We discuessed two minor issues on IRC (NotFound error instead of UnexpectedFormData in HasCustomlagnuageCodeTraversalMixin, and a no longer needed definition of getCustomLangaugeCode in IDistribution), pleased fix them.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/registry/browser/distributionsourcepackage.py'
2--- lib/lp/registry/browser/distributionsourcepackage.py 2009-11-10 21:33:20 +0000
3+++ lib/lp/registry/browser/distributionsourcepackage.py 2009-11-19 04:05:29 +0000
4@@ -50,6 +50,8 @@
5 from lp.registry.browser.packaging import PackagingDeleteView
6 from lp.registry.interfaces.pocket import pocketsuffix
7 from lp.registry.interfaces.product import IDistributionSourcePackage
8+from lp.translations.browser.customlanguagecode import (
9+ HasCustomLanguageCodesTraversalMixin)
10
11
12 class DistributionSourcePackageBreadcrumb(Breadcrumb):
13@@ -117,7 +119,8 @@
14
15
16 class DistributionSourcePackageNavigation(Navigation,
17- BugTargetTraversalMixin, QuestionTargetTraversalMixin,
18+ BugTargetTraversalMixin, HasCustomLanguageCodesTraversalMixin,
19+ QuestionTargetTraversalMixin,
20 StructuralSubscriptionTargetTraversalMixin):
21
22 usedfor = IDistributionSourcePackage
23
24=== modified file 'lib/lp/registry/browser/product.py'
25--- lib/lp/registry/browser/product.py 2009-11-11 16:57:29 +0000
26+++ lib/lp/registry/browser/product.py 2009-11-19 04:05:29 +0000
27@@ -86,6 +86,8 @@
28 from lp.answers.browser.faqtarget import FAQTargetNavigationMixin
29 from canonical.launchpad.browser.feeds import FeedsMixin
30 from lp.registry.browser.productseries import get_series_branch_error
31+from lp.translations.browser.customlanguagecode import (
32+ HasCustomLanguageCodesTraversalMixin)
33 from canonical.launchpad.browser.multistep import MultiStepView, StepView
34 from lp.answers.browser.questiontarget import (
35 QuestionTargetFacetMixin, QuestionTargetTraversalMixin)
36@@ -118,8 +120,8 @@
37
38 class ProductNavigation(
39 Navigation, BugTargetTraversalMixin,
40- FAQTargetNavigationMixin, QuestionTargetTraversalMixin,
41- StructuralSubscriptionTargetTraversalMixin):
42+ FAQTargetNavigationMixin, HasCustomLanguageCodesTraversalMixin,
43+ QuestionTargetTraversalMixin, StructuralSubscriptionTargetTraversalMixin):
44
45 usedfor = IProduct
46
47
48=== modified file 'lib/lp/registry/configure.zcml'
49--- lib/lp/registry/configure.zcml 2009-11-15 01:05:49 +0000
50+++ lib/lp/registry/configure.zcml 2009-11-19 04:05:29 +0000
51@@ -353,6 +353,9 @@
52 <class
53 class="lp.registry.model.distributionsourcepackage.DistributionSourcePackage">
54 <allow
55+ interface="lp.translations.interfaces.customlanguagecode.IHasCustomLanguageCodes"/>
56+
57+ <allow
58 attributes="
59 distribution
60 development_version
61@@ -1045,6 +1048,8 @@
62 interface="lp.registry.interfaces.product.IProductPublic"/>
63 <allow
64 interface="lp.translations.interfaces.translationimportqueue.IHasTranslationImports"/>
65+ <allow
66+ interface="lp.translations.interfaces.customlanguagecode.IHasCustomLanguageCodes"/>
67 <require
68 permission="launchpad.Driver"
69 interface="lp.registry.interfaces.product.IProductDriverRestricted"/>
70
71=== modified file 'lib/lp/registry/interfaces/distribution.py'
72--- lib/lp/registry/interfaces/distribution.py 2009-10-26 18:40:04 +0000
73+++ lib/lp/registry/interfaces/distribution.py 2009-11-19 04:05:29 +0000
74@@ -511,13 +511,6 @@
75 bug watches or to products that use_malone.
76 """
77
78- def getCustomLanguageCode(sourcepackagename, language_code):
79- """Look up `ICustomLanguageCode`.
80-
81- A `SourcePackageName` in a Distribution may override some
82- language codes for translation import purposes.
83- """
84-
85 def userCanEdit(user):
86 """Can the user edit this distribution?"""
87
88
89=== modified file 'lib/lp/registry/interfaces/product.py'
90--- lib/lp/registry/interfaces/product.py 2009-10-26 18:40:04 +0000
91+++ lib/lp/registry/interfaces/product.py 2009-11-19 04:05:29 +0000
92@@ -668,13 +668,6 @@
93 def packagedInDistros():
94 """Returns the distributions this product has been packaged in."""
95
96- def getCustomLanguageCode(language_code):
97- """Look up `ICustomLanguageCode` for `language_code`, if any.
98-
99- Products may override language code definitions for translation
100- import purposes.
101- """
102-
103 def userCanEdit(user):
104 """Can the user edit this product?"""
105
106
107=== modified file 'lib/lp/registry/model/distribution.py'
108--- lib/lp/registry/model/distribution.py 2009-11-06 21:10:13 +0000
109+++ lib/lp/registry/model/distribution.py 2009-11-19 04:05:29 +0000
110@@ -37,7 +37,6 @@
111 BugTargetBase, OfficialBugTagTargetMixin)
112 from lp.bugs.model.bugtask import BugTask
113 from lp.soyuz.model.build import Build
114-from lp.translations.model.customlanguagecode import CustomLanguageCode
115 from lp.registry.model.distributionmirror import DistributionMirror
116 from lp.registry.model.distributionsourcepackage import (
117 DistributionSourcePackage)
118@@ -1458,12 +1457,6 @@
119 bugs_affecting_upstream, bugs_with_upstream_bugwatch))
120 return results
121
122- def getCustomLanguageCode(self, sourcepackagename, language_code):
123- """See `IDistribution`."""
124- return CustomLanguageCode.selectOneBy(
125- distribution=self, sourcepackagename=sourcepackagename,
126- language_code=language_code)
127-
128 def setBugSupervisor(self, bug_supervisor, user):
129 """See `IHasBugSupervisor`."""
130 self.bug_supervisor = bug_supervisor
131
132=== modified file 'lib/lp/registry/model/distributionsourcepackage.py'
133--- lib/lp/registry/model/distributionsourcepackage.py 2009-11-17 21:14:55 +0000
134+++ lib/lp/registry/model/distributionsourcepackage.py 2009-11-19 04:05:29 +0000
135@@ -44,11 +44,18 @@
136 DistributionSourcePackageRelease)
137 from lp.soyuz.model.publishing import SourcePackagePublishingHistory
138 from lp.soyuz.model.sourcepackagerelease import SourcePackageRelease
139+from lp.translations.interfaces.customlanguagecode import (
140+ IHasCustomLanguageCodes)
141+from lp.translations.model.customlanguagecode import (
142+ CustomLanguageCode, HasCustomLanguageCodesMixin)
143+
144
145 class DistributionSourcePackage(BugTargetBase,
146 SourcePackageQuestionTargetMixin,
147 StructuralSubscriptionTargetMixin,
148- HasBranchesMixin, HasMergeProposalsMixin):
149+ HasBranchesMixin,
150+ HasCustomLanguageCodesMixin,
151+ HasMergeProposalsMixin):
152 """This is a "Magic Distribution Source Package". It is not an
153 SQLObject, but instead it represents a source package with a particular
154 name in a particular distribution. You can then ask it all sorts of
155@@ -56,7 +63,8 @@
156 or current release, etc.
157 """
158
159- implements(IDistributionSourcePackage, IQuestionTarget)
160+ implements(
161+ IDistributionSourcePackage, IHasCustomLanguageCodes, IQuestionTarget)
162
163 def __init__(self, distribution, sourcepackagename):
164 self.distribution = distribution
165@@ -413,6 +421,19 @@
166 'BugTask.distribution = %s AND BugTask.sourcepackagename = %s' %
167 sqlvalues(self.distribution, self.sourcepackagename))
168
169+ def composeCustomLanguageCodeMatch(self):
170+ """See `HasCustomLanguageCodesMixin`."""
171+ return And(
172+ CustomLanguageCode.distribution == self.distribution,
173+ CustomLanguageCode.sourcepackagename == self.sourcepackagename)
174+
175+ def createCustomLanguageCode(self, language_code, language):
176+ """See `IHasCustomLanguageCodes`."""
177+ return CustomLanguageCode(
178+ distribution=self.distribution,
179+ sourcepackagename=self.sourcepackagename,
180+ language_code=language_code, language=language)
181+
182 @staticmethod
183 def getPersonsByEmail(email_addresses):
184 """[(EmailAddress,Person), ..] iterable for given email addresses."""
185
186=== modified file 'lib/lp/registry/model/product.py'
187--- lib/lp/registry/model/product.py 2009-11-06 21:06:38 +0000
188+++ lib/lp/registry/model/product.py 2009-11-19 04:05:29 +0000
189@@ -1,6 +1,5 @@
190 # Copyright 2009 Canonical Ltd. This software is licensed under the
191 # GNU Affero General Public License version 3 (see the file LICENSE).
192-
193 # pylint: disable-msg=E0611,W0212
194
195 """Database classes including and related to Product."""
196@@ -45,7 +44,8 @@
197 from lp.bugs.model.bugwatch import BugWatch
198 from lp.registry.model.commercialsubscription import (
199 CommercialSubscription)
200-from lp.translations.model.customlanguagecode import CustomLanguageCode
201+from lp.translations.model.customlanguagecode import (
202+ CustomLanguageCode, HasCustomLanguageCodesMixin)
203 from lp.translations.model.potemplate import POTemplate
204 from lp.registry.model.distroseries import DistroSeries
205 from lp.registry.model.distribution import Distribution
206@@ -81,6 +81,8 @@
207 from canonical.launchpad.interfaces.launchpad import (
208 IHasIcon, IHasLogo, IHasMugshot, ILaunchpadCelebrities, ILaunchpadUsage,
209 NotFoundError)
210+from lp.translations.interfaces.customlanguagecode import (
211+ IHasCustomLanguageCodes)
212 from canonical.launchpad.interfaces.launchpadstatistic import (
213 ILaunchpadStatisticSet)
214 from lp.registry.interfaces.person import IPersonSet
215@@ -166,13 +168,13 @@
216 QuestionTargetMixin, HasTranslationImportsMixin,
217 HasAliasMixin, StructuralSubscriptionTargetMixin,
218 HasMilestonesMixin, OfficialBugTagTargetMixin, HasBranchesMixin,
219- HasMergeProposalsMixin):
220+ HasCustomLanguageCodesMixin, HasMergeProposalsMixin):
221
222 """A Product."""
223
224 implements(
225- IFAQTarget, IHasBugSupervisor, IHasIcon, IHasLogo,
226- IHasMugshot, ILaunchpadUsage, IProduct, IQuestionTarget)
227+ IFAQTarget, IHasBugSupervisor, IHasCustomLanguageCodes, IHasIcon,
228+ IHasLogo, IHasMugshot, ILaunchpadUsage, IProduct, IQuestionTarget)
229
230 _table = 'Product'
231
232@@ -964,10 +966,14 @@
233 if bug_supervisor is not None:
234 subscription = self.addBugSubscription(bug_supervisor, user)
235
236- def getCustomLanguageCode(self, language_code):
237- """See `IProduct`."""
238- return CustomLanguageCode.selectOneBy(
239- product=self, language_code=language_code)
240+ def composeCustomLanguageCodeMatch(self):
241+ """See `HasCustomLanguageCodesMixin`."""
242+ return CustomLanguageCode.product == self
243+
244+ def createCustomLanguageCode(self, language_code, language):
245+ """See `IHasCustomLanguageCodes`."""
246+ return CustomLanguageCode(
247+ product=self, language_code=language_code, language=language)
248
249 def userCanEdit(self, user):
250 """See `IProduct`."""
251
252=== modified file 'lib/lp/translations/browser/configure.zcml'
253--- lib/lp/translations/browser/configure.zcml 2009-10-31 12:03:43 +0000
254+++ lib/lp/translations/browser/configure.zcml 2009-11-19 04:05:29 +0000
255@@ -977,5 +977,50 @@
256 name="+language-packs"
257 template="../templates/distroseries-language-packs.pt"/>
258
259+
260+<!-- CustomLanguageCode -->
261+
262+ <browser:defaultView
263+ for="lp.translations.interfaces.customlanguagecode.ICustomLanguageCode"
264+ name="+index"
265+ layer="canonical.launchpad.layers.TranslationsLayer"/>
266+ <browser:url
267+ for="lp.translations.interfaces.customlanguagecode.ICustomLanguageCode"
268+ path_expression="string:+customcode/${language_code}"
269+ attribute_to_parent="translation_target"
270+ />
271+ <browser:page
272+ name="+index"
273+ for="lp.translations.interfaces.customlanguagecode.ICustomLanguageCode"
274+ permission="zope.Public"
275+ class="lp.translations.browser.customlanguagecode.CustomLanguageCodeView"
276+ template="../templates/customlanguagecode-index.pt"
277+ layer="canonical.launchpad.layers.TranslationsLayer"/>
278+
279+ <browser:page
280+ name="+remove"
281+ for="lp.translations.interfaces.customlanguagecode.ICustomLanguageCode"
282+ permission="launchpad.Admin"
283+ class="lp.translations.browser.customlanguagecode.CustomLanguageCodeRemoveView"
284+ template="../../app/templates/generic-edit.pt"
285+ layer="canonical.launchpad.layers.TranslationsLayer"/>
286+
287+<!-- IHasCustomLanguageCodes -->
288+
289+ <browser:page
290+ name="+custom-language-codes"
291+ for="lp.translations.interfaces.customlanguagecode.IHasCustomLanguageCodes"
292+ layer="canonical.launchpad.layers.TranslationsLayer"
293+ class="lp.translations.browser.customlanguagecode.CustomLanguageCodesIndexView"
294+ template="../templates/customlanguagecodes-index.pt"
295+ permission="zope.Public"/>
296+ <browser:page
297+ name="+add-custom-language-code"
298+ for="lp.translations.interfaces.customlanguagecode.IHasCustomLanguageCodes"
299+ layer="canonical.launchpad.layers.TranslationsLayer"
300+ class="lp.translations.browser.customlanguagecode.CustomLanguageCodeAddView"
301+ template="../templates/customlanguagecode-add.pt"
302+ permission="launchpad.Admin"/>
303+
304 </facet>
305 </configure>
306
307=== added file 'lib/lp/translations/browser/customlanguagecode.py'
308--- lib/lp/translations/browser/customlanguagecode.py 1970-01-01 00:00:00 +0000
309+++ lib/lp/translations/browser/customlanguagecode.py 2009-11-19 04:05:29 +0000
310@@ -0,0 +1,171 @@
311+# Copyright 2009 Canonical Ltd. This software is licensed under the
312+# GNU Affero General Public License version 3 (see the file LICENSE).
313+
314+__metaclass__ = type
315+
316+__all__ = [
317+ 'CustomLanguageCodeAddView',
318+ 'CustomLanguageCodeBreadcrumb',
319+ 'CustomLanguageCodesIndexView',
320+ 'CustomLanguageCodeRemoveView',
321+ 'CustomLanguageCodeView',
322+ 'HasCustomLanguageCodesNavigation',
323+ 'HasCustomLanguageCodesTraversalMixin',
324+ ]
325+
326+
327+import re
328+
329+from canonical.lazr.utils import smartquote
330+
331+from lp.translations.interfaces.customlanguagecode import (
332+ ICustomLanguageCode, IHasCustomLanguageCodes)
333+
334+from canonical.launchpad.webapp import (
335+ action, canonical_url, LaunchpadFormView, LaunchpadView, Navigation,
336+ stepthrough)
337+from canonical.launchpad.webapp.breadcrumb import Breadcrumb
338+from canonical.launchpad.webapp.interfaces import NotFoundError
339+from canonical.launchpad.webapp.menu import structured
340+
341+
342+# Regex for allowable custom language codes.
343+CODE_PATTERN = "[a-zA-Z0-9_-]+$"
344+
345+
346+def check_code(custom_code):
347+ """Is this custom language code well-formed?"""
348+ return re.match(CODE_PATTERN, custom_code) is not None
349+
350+
351+class CustomLanguageCodeBreadcrumb(Breadcrumb):
352+ """Breadcrumb for a `CustomLanguageCode`."""
353+ @property
354+ def text(self):
355+ return smartquote(
356+ 'Custom language code "%s"' % self.context.language_code)
357+
358+
359+class CustomLanguageCodesIndexView(LaunchpadView):
360+ """Listing of `CustomLanguageCode`s for a given context."""
361+
362+ page_title = "Custom language codes"
363+
364+ @property
365+ def label(self):
366+ return "Custom language codes for %s" % self.context.displayname
367+
368+
369+class CustomLanguageCodeAddView(LaunchpadFormView):
370+ """Create a new custom language code."""
371+ schema = ICustomLanguageCode
372+ field_names = ['language_code', 'language']
373+ page_title = "Add new code"
374+
375+ create = False
376+
377+ @property
378+ def label(self):
379+ return (
380+ "Add a custom language code for %s" % self.context.displayname)
381+
382+ def validate(self, data):
383+ self.language_code = data.get('language_code')
384+ self.language = data.get('language')
385+ if self.language_code is not None:
386+ self.language_code = self.language_code.strip()
387+
388+ if not self.language_code:
389+ self.setFieldError('language_code', "No code was entered.")
390+ return
391+
392+ if not check_code(self.language_code):
393+ self.setFieldError('language_code', "Invalid language code.")
394+ return
395+
396+ existing_code = self.context.getCustomLanguageCode(self.language_code)
397+ if existing_code is not None:
398+ if existing_code.language != self.language:
399+ self.setFieldError(
400+ 'language_code',
401+ structured(
402+ "There already is a custom language code '%s'." %
403+ self.language_code))
404+ return
405+ else:
406+ self.create = True
407+
408+ @action('Add', name='add')
409+ def add_action(self, action, data):
410+ if self.create:
411+ self.context.createCustomLanguageCode(
412+ self.language_code, self.language)
413+
414+ @property
415+ def action_url(self):
416+ return "%s/+add-custom-language-code" % canonical_url(self.context)
417+
418+ @property
419+ def next_url(self):
420+ """See `LaunchpadFormView`."""
421+ return "%s/+custom-language-codes" % canonical_url(self.context)
422+
423+ @property
424+ def cancel_url(self):
425+ return self.next_url
426+
427+
428+class CustomLanguageCodeView(LaunchpadView):
429+ schema = ICustomLanguageCode
430+
431+
432+class CustomLanguageCodeRemoveView(LaunchpadFormView):
433+ """View for removing a `CustomLanguageCode`."""
434+ schema = ICustomLanguageCode
435+ field_names = []
436+
437+ page_title = "Remove"
438+
439+ @property
440+ def code(self):
441+ """The custom code."""
442+ return self.context.language_code
443+
444+ @property
445+ def label(self):
446+ return "Remove custom language code '%s'" % self.code
447+
448+ @action("Remove")
449+ def remove(self, action, data):
450+ """Remove this `CustomLanguageCode`."""
451+ code = self.code
452+ self.context.translation_target.removeCustomLanguageCode(self.context)
453+ self.request.response.addInfoNotification(
454+ "Removed custom language code '%s'." % code)
455+
456+ @property
457+ def next_url(self):
458+ return "%s/+custom-language-codes" % canonical_url(
459+ self.context.translation_target)
460+
461+ @property
462+ def cancel_url(self):
463+ return self.next_url
464+
465+
466+class HasCustomLanguageCodesTraversalMixin:
467+ """Navigate from an `IHasCustomLanguageCodes` to a `CustomLanguageCode`.
468+ """
469+ @stepthrough('+customcode')
470+ def traverseCustomCode(self, name):
471+ """Traverse +customcode URLs."""
472+ if not check_code(name):
473+ raise NotFoundError("Invalid custom language code.")
474+
475+ return self.context.getCustomLanguageCode(name)
476+
477+
478+class HasCustomLanguageCodesNavigation(Navigation,
479+ HasCustomLanguageCodesTraversalMixin):
480+ """Generic navigation for `IHasCustomLanguageCodes`."""
481+ usedfor = IHasCustomLanguageCodes
482
483=== modified file 'lib/lp/translations/configure.zcml'
484--- lib/lp/translations/configure.zcml 2009-09-17 14:45:59 +0000
485+++ lib/lp/translations/configure.zcml 2009-11-19 04:05:29 +0000
486@@ -549,6 +549,11 @@
487 interface="lp.translations.interfaces.translationmessage.ITranslationMessageSet"/>
488 </securedutility>
489 </facet>
490+ <adapter
491+ provides="canonical.launchpad.webapp.interfaces.IBreadcrumb"
492+ for="lp.translations.interfaces.customlanguagecode.ICustomLanguageCode"
493+ factory="lp.translations.browser.customlanguagecode.CustomLanguageCodeBreadcrumb"
494+ permission="zope.Public"/>
495 <class
496 class="lp.translations.model.customlanguagecode.CustomLanguageCode">
497 <allow
498
499=== modified file 'lib/lp/translations/interfaces/customlanguagecode.py'
500--- lib/lp/translations/interfaces/customlanguagecode.py 2009-07-17 00:26:05 +0000
501+++ lib/lp/translations/interfaces/customlanguagecode.py 2009-11-19 04:05:29 +0000
502@@ -1,5 +1,6 @@
503 # Copyright 2009 Canonical Ltd. This software is licensed under the
504 # GNU Affero General Public License version 3 (see the file LICENSE).
505+# pylint: disable-msg=E0213
506
507 """Custom language code."""
508
509@@ -7,22 +8,76 @@
510
511 __all__ = [
512 'ICustomLanguageCode',
513+ 'IHasCustomLanguageCodes',
514 ]
515
516-from zope.interface import Interface, Attribute
517-from zope.schema import Int, TextLine
518+from zope.interface import Interface
519+from zope.schema import Bool, Choice, Int, Object, Set, TextLine
520+from zope.schema.interfaces import IObject
521
522 from canonical.launchpad import _
523
524+from lp.registry.interfaces.distribution import IDistribution
525+from lp.registry.interfaces.product import IProduct
526+from lp.registry.interfaces.sourcepackagename import ISourcePackageName
527+
528
529 class ICustomLanguageCode(Interface):
530 """`CustomLanguageCode` interface."""
531
532- id = Int(title=_(u"ID"), required=True, readonly=True)
533- product = Attribute(_(u"Product"))
534- distribution = Attribute(_(u"Distribution"))
535- sourcepackagename = Attribute(_(u"Source package name"))
536- language_code = TextLine(title=_(u"Language code"), required=True,
537- description=_("Language code to treat as special."))
538- language = Attribute(_(u"Language"))
539-
540+ id = Int(title=_("ID"), required=True, readonly=True)
541+ product = Object(
542+ title=_("Product"), required=False, readonly=True, schema=IProduct)
543+ distribution = Object(
544+ title=_("Distribution"), required=False, readonly=True,
545+ schema=IDistribution)
546+ sourcepackagename = Object(
547+ title=_("Source package name"), required=False, readonly=True,
548+ schema=ISourcePackageName)
549+ language_code = TextLine(title=_("Language code"),
550+ description=_("Language code to treat as special."),
551+ required=True, readonly=False)
552+ language = Choice(
553+ title=_("Language"), required=False, readonly=False,
554+ vocabulary='Language',
555+ description=_("Language to map this code to. "
556+ "Leave empty to drop translations for this code."))
557+
558+ # Reference back to the IHasCustomLanguageCodes.
559+ translation_target = Object(
560+ title=_("Context this custom language code applies to"),
561+ required=True, readonly=True, schema=IObject)
562+
563+
564+class IHasCustomLanguageCodes(Interface):
565+ """A context that can have custom language codes attached.
566+
567+ Implemented by `Product` and `SourcePackage`.
568+ """
569+ custom_language_codes = Set(
570+ title=_("Custom language codes"),
571+ description=_("Translations for these language codes are re-routed."),
572+ value_type=Object(schema=ICustomLanguageCode),
573+ required=False, readonly=False)
574+
575+ has_custom_language_codes = Bool(
576+ title=_("There are custom language codes in this context."),
577+ readonly=True, required=True)
578+
579+ def getCustomLanguageCode(language_code):
580+ """Retrieve `CustomLanguageCode` for `language_code`.
581+
582+ :return: a `CustomLanguageCode`, or None.
583+ """
584+
585+ def createCustomLanguageCode(language_code, language):
586+ """Create `CustomLanguageCode`.
587+
588+ :return: the new `CustomLanguageCode` object.
589+ """
590+
591+ def removeCustomLanguageCode(language_code):
592+ """Remove `CustomLanguageCode`.
593+
594+ :param language_code: A `CustomLanguageCode` object.
595+ """
596
597=== modified file 'lib/lp/translations/model/customlanguagecode.py'
598--- lib/lp/translations/model/customlanguagecode.py 2009-07-17 00:26:05 +0000
599+++ lib/lp/translations/model/customlanguagecode.py 2009-11-19 04:05:29 +0000
600@@ -5,14 +5,23 @@
601
602 __metaclass__ = type
603
604-__all__ = ['CustomLanguageCode']
605-
606+__all__ = [
607+ 'CustomLanguageCode',
608+ 'HasCustomLanguageCodesMixin',
609+ ]
610+
611+
612+from zope.interface import implements
613
614 from sqlobject import ForeignKey, StringCol
615-from zope.interface import implements
616+from storm.expr import And
617
618 from canonical.database.sqlbase import SQLBase
619+from canonical.launchpad.interfaces.lpstorm import IStore
620+
621 from lp.translations.interfaces.customlanguagecode import ICustomLanguageCode
622+
623+
624 class CustomLanguageCode(SQLBase):
625 """See `ICustomLanguageCode`."""
626
627@@ -32,3 +41,63 @@
628 language = ForeignKey(
629 dbName='language', foreignKey='Language', notNull=False, default=None)
630
631+ @property
632+ def translation_target(self):
633+ """See `ICustomLanguageCode`."""
634+ # Avoid circular imports
635+ from lp.registry.model.distributionsourcepackage import (
636+ DistributionSourcePackage)
637+ if self.product:
638+ return self.product
639+ else:
640+ return DistributionSourcePackage(
641+ self.distribution, self.sourcepackagename)
642+
643+
644+class HasCustomLanguageCodesMixin:
645+ """Helper class to implement `IHasCustomLanguageCodes`."""
646+
647+ def composeCustomLanguageCodeMatch(self):
648+ """Define in child: compose Storm match clause.
649+
650+ This should return a condition for use in a Storm query to match
651+ `CustomLanguageCode` objects to `self`.
652+ """
653+ raise NotImplementedError("composeCustomLanguageCodeMatch")
654+
655+ def createCustomLanguageCode(self, language_code, language):
656+ """Define in child. See `IHasCustomLanguageCodes`."""
657+ raise NotImplementedError("createCustomLanguageCode")
658+
659+ def _queryCustomLanguageCodes(self, language_code=None):
660+ """Query `CustomLanguageCodes` belonging to `self`.
661+
662+ :param language_code: Optional custom language code to look for.
663+ If not given, all codes will match.
664+ :return: A Storm result set.
665+ """
666+ match = self.composeCustomLanguageCodeMatch()
667+ store = IStore(CustomLanguageCode)
668+ if language_code is not None:
669+ match = And(
670+ match, CustomLanguageCode.language_code == language_code)
671+ return store.find(CustomLanguageCode, match)
672+
673+ @property
674+ def has_custom_language_codes(self):
675+ """See `IHasCustomLanguageCodes`."""
676+ return self._queryCustomLanguageCodes().any() is not None
677+
678+ @property
679+ def custom_language_codes(self):
680+ """See `IHasCustomLanguageCodes`."""
681+ return self._queryCustomLanguageCodes().order_by('language_code')
682+
683+ def getCustomLanguageCode(self, language_code):
684+ """See `IHasCustomLanguageCodes`."""
685+ return self._queryCustomLanguageCodes(language_code).one()
686+
687+ def removeCustomLanguageCode(self, custom_code):
688+ """See `IHasCustomLanguageCodes`."""
689+ language_code = custom_code.language_code
690+ return self._queryCustomLanguageCodes(language_code).remove()
691
692=== modified file 'lib/lp/translations/model/translationimportqueue.py'
693--- lib/lp/translations/model/translationimportqueue.py 2009-11-18 11:45:59 +0000
694+++ lib/lp/translations/model/translationimportqueue.py 2009-11-19 04:05:29 +0000
695@@ -342,11 +342,12 @@
696 def _findCustomLanguageCode(self, language_code):
697 """Find applicable custom language code, if any."""
698 if self.distroseries is not None:
699- return self.distroseries.distribution.getCustomLanguageCode(
700- self.sourcepackagename, language_code)
701+ target = self.distroseries.distribution.getSourcePackage(
702+ self.sourcepackagename)
703 else:
704- return self.productseries.product.getCustomLanguageCode(
705- language_code)
706+ target = self.productseries.product
707+
708+ return target.getCustomLanguageCode(language_code)
709
710 def _guessLanguage(self):
711 """See ITranslationImportQueueEntry."""
712
713=== added file 'lib/lp/translations/stories/standalone/custom-language-codes.txt'
714--- lib/lp/translations/stories/standalone/custom-language-codes.txt 1970-01-01 00:00:00 +0000
715+++ lib/lp/translations/stories/standalone/custom-language-codes.txt 2009-11-19 04:05:29 +0000
716@@ -0,0 +1,276 @@
717+Custom Language Codes
718+---------------------
719+
720+Some projects insist on using nonstandard language codes, such as es_ES
721+for standard Spanish or pt-BR instead of pt_BR. Custom language codes
722+are a feature that helps deal with this during translation import. A
723+custom language code maps a language code as the project (or package)
724+uses it to a language, regardless of whether the code is for an existing
725+language or not.
726+
727+Custom language codes are attached to either a product or a source
728+package.
729+
730+ >>> import re
731+ >>> from zope.component import getUtility
732+ >>> from zope.security.proxy import removeSecurityProxy
733+ >>> from canonical.launchpad.interfaces.launchpad import (
734+ ... ILaunchpadCelebrities)
735+
736+ >>> def find_custom_language_codes_link(browser):
737+ ... """Find reference to custom language codes on a page."""
738+ ... return find_tag_by_id(browser.contents, 'custom-language-codes')
739+
740+ >>> login(ANONYMOUS)
741+ >>> owner = factory.makePerson(email='o@example.com', password='test')
742+ >>> rosetta_admin = factory.makePerson(
743+ ... email='r@example.com', password='test')
744+ >>> rosetta_admin.join(getUtility(ILaunchpadCelebrities).rosetta_experts)
745+ >>> product = factory.makeProduct(displayname="Foo", owner=owner)
746+ >>> trunk = product.getSeries('trunk')
747+ >>> removeSecurityProxy(product).official_rosetta = True
748+ >>> template = factory.makePOTemplate(productseries=trunk)
749+ >>> product_page = canonical_url(product, rootsite='translations')
750+ >>> logout()
751+
752+ >>> owner_browser = setupBrowser("Basic o@example.com:test")
753+
754+An administrator sees the link to the custom language codes on a
755+project's main translations page.
756+
757+ >>> admin_browser.open(product_page)
758+ >>> tag = find_custom_language_codes_link(admin_browser)
759+ >>> print extract_text(tag.renderContents())
760+ If necessary, you may
761+ define custom language codes
762+ for this project.
763+
764+The link goes to the custom language codes management page.
765+
766+ >>> admin_browser.getLink("define custom language codes").click()
767+ >>> custom_language_codes_page = admin_browser.url
768+
769+Non-admins, even the project's owner, don't see this link. We do not
770+advertise this feature, since the proper solution is generally to use
771+the right language codes.
772+
773+ >>> owner_browser.open(product_page)
774+ >>> print find_custom_language_codes_link(owner_browser)
775+ None
776+
777+Initially the page shows no custom language codes for the project.
778+
779+ >>> tag = find_tag_by_id(admin_browser.contents, 'empty')
780+ >>> print extract_text(tag.renderContents())
781+ No custom language codes have been defined.
782+
783+The admin can add a custom language code.
784+
785+ >>> admin_browser.getLink("Add a custom language code").click()
786+ >>> add_page = admin_browser.url
787+
788+ >>> admin_browser.getControl("Language code:").value = 'no'
789+ >>> admin_browser.getControl("Language:").value = ['nn']
790+ >>> admin_browser.getControl("Add").click()
791+
792+This leads back to the custom language codes overview, where the new
793+code is now shown.
794+
795+ >>> admin_browser.url == custom_language_codes_page
796+ True
797+
798+ >>> tag = find_tag_by_id(admin_browser.contents, 'nonempty')
799+ >>> print extract_text(tag.renderContents())
800+ Foo uses the following custom language codes:
801+ Code... ...maps to language
802+ no Norwegian Nynorsk
803+
804+There is an overview page for the custom code, though there's not much
805+to see there.
806+
807+ >>> admin_browser.getLink("no").click()
808+ >>> main = find_main_content(admin_browser.contents)
809+ >>> print extract_text(main.renderContents())
810+ Foo Translations Custom language code ...no...
811+ For Foo, uploads with the language code
812+ &ldquo;no&rdquo;
813+ are associated with the language
814+ Norwegian Nynorsk.
815+ remove custom language code
816+ custom language codes overview
817+
818+The overview page leads back to the custom language codes overview.
819+
820+ >>> code_page = admin_browser.url
821+ >>> admin_browser.getLink("custom language codes overview").click()
822+ >>> admin_browser.url == custom_language_codes_page
823+ True
824+
825+ >>> admin_browser.open(code_page)
826+
827+There is also a link for removing codes. The admin follows the link and
828+removes the "no" custom language code.
829+
830+ >>> admin_browser.getLink("remove custom language code").click()
831+ >>> remove_page = admin_browser.url
832+ >>> admin_browser.getControl("Remove").click()
833+
834+This leads back to the overview page.
835+
836+ >>> admin_browser.url == custom_language_codes_page
837+ True
838+
839+ >>> tag = find_tag_by_id(admin_browser.contents, 'empty')
840+ >>> print extract_text(tag.renderContents())
841+ No custom language codes have been defined.
842+
843+
844+Non-admin access
845+================
846+
847+A non-admin can see the page, actually, if they know the URL. This can
848+be convenient for debugging.
849+
850+ >>> owner_browser.open(custom_language_codes_page)
851+
852+ >>> tag = find_tag_by_id(owner_browser.contents, 'empty')
853+ >>> print extract_text(tag.renderContents())
854+ No custom language codes have been defined.
855+
856+However all they get is a read-only version of the page.
857+
858+ >>> owner_browser.getLink("Add a custom language code").click()
859+ Traceback (most recent call last):
860+ ...
861+ LinkNotFoundError
862+
863+The page for adding custom language codes is not accessible to them.
864+
865+ >>> owner_browser.open(add_page)
866+ Traceback (most recent call last):
867+ ...
868+ Unauthorized: ...
869+
870+And naturally, if an admin creates a custom language code again, a
871+non-admin can't remove it.
872+
873+ >>> admin_browser.open(add_page)
874+ >>> admin_browser.getControl("Language code:").value = 'no'
875+ >>> admin_browser.getControl("Language:").value = ['nn']
876+ >>> admin_browser.getControl("Add").click()
877+
878+ >>> owner_browser.open(custom_language_codes_page)
879+ >>> tag = find_tag_by_id(owner_browser.contents, 'nonempty')
880+ >>> print extract_text(tag.renderContents())
881+ Foo uses the following custom language codes:
882+ Code... ...maps to language
883+ no Norwegian Nynorsk
884+
885+ >>> owner_browser.getLink("no").click()
886+ >>> owner_browser.getLink("remove custom language code")
887+ Traceback (most recent call last):
888+ ...
889+ LinkNotFoundError
890+
891+ >>> owner_browser.open(remove_page)
892+ Traceback (most recent call last):
893+ ...
894+ Unauthorized: ...
895+
896+
897+Source packages
898+===============
899+
900+The story for source packages is very similar to that for products. In
901+this case, the custom language code is tied to the distribution source
902+package--i.e. the combination of a distribution and a source package
903+name. However, since there is no Translations page for that type of
904+object (and we'd probably never go there if there were), the link is
905+shown on the source package page.
906+
907+ >>> login(ANONYMOUS)
908+ >>> from lp.registry.model.sourcepackage import SourcePackage
909+ >>> from lp.registry.model.sourcepackagename import SourcePackageName
910+
911+ >>> distro = factory.makeDistribution('distro')
912+ >>> distroseries = factory.makeDistroRelease(distribution=distro)
913+ >>> sourcepackagename = SourcePackageName(name='bar')
914+ >>> package = factory.makeSourcePackage(
915+ ... sourcepackagename=sourcepackagename, distroseries=distroseries)
916+ >>> removeSecurityProxy(distro).official_rosetta = True
917+ >>> other_series = factory.makeDistroRelease(distribution=distro)
918+ >>> template = factory.makePOTemplate(
919+ ... distroseries=package.distroseries,
920+ ... sourcepackagename=package.sourcepackagename)
921+ >>> package_page = canonical_url(package, rootsite="translations")
922+ >>> page_in_other_series = canonical_url(SourcePackage(
923+ ... distroseries=other_series,
924+ ... sourcepackagename=package.sourcepackagename),
925+ ... rootsite="translations")
926+ >>> logout()
927+
928+ >>> admin_browser.open(package_page)
929+
930+Of course in this case, the notice about there being no custom language
931+codes talks about a package, not a project.
932+
933+ >>> tag = find_custom_language_codes_link(admin_browser)
934+ >>> print extract_text(tag.renderContents())
935+ If necessary, you may
936+ define custom language codes
937+ for this package.
938+
939+ >>> admin_browser.getLink("define custom language codes").click()
940+ >>> custom_language_codes_page = admin_browser.url
941+
942+ >>> tag = find_tag_by_id(admin_browser.contents, 'empty')
943+ >>> print extract_text(tag.renderContents())
944+ No custom language codes have been defined.
945+
946+Again, an admin can add a language code.
947+
948+ >>> admin_browser.getLink("Add a custom language code").click()
949+ >>> add_page = admin_browser.url
950+
951+ >>> admin_browser.getControl("Language code:").value = 'pt-br'
952+ >>> admin_browser.getControl("Language:").value = ['pt_BR']
953+ >>> admin_browser.getControl("Add").click()
954+
955+The language code is displayed.
956+
957+ >>> tag = find_tag_by_id(admin_browser.contents, 'nonempty')
958+ >>> print extract_text(tag.renderContents())
959+ bar in distro uses the following custom language codes:
960+ Code... ...maps to language
961+ pt-br Portuguese (Brazil)
962+
963+It's also displayed identically on the same package but in another
964+release series of the same distribution.
965+
966+ >>> admin_browser.open(page_in_other_series)
967+ >>> tag = find_custom_language_codes_link(admin_browser)
968+ >>> print extract_text(tag.renderContents())
969+ If necessary, you may
970+ define custom language codes
971+ for this package.
972+
973+ >>> admin_browser.getLink("define custom language codes").click()
974+ >>> tag = find_tag_by_id(admin_browser.contents, 'nonempty')
975+ >>> print extract_text(tag.renderContents())
976+ bar in distro uses the following custom language codes:
977+ Code... ...maps to language
978+ pt-br Portuguese (Brazil)
979+
980+
981+The new code has a link there...
982+
983+ >>> admin_browser.getLink("pt-br").click()
984+
985+...and can be deleted.
986+
987+ >>> admin_browser.getLink("remove custom language code").click()
988+ >>> admin_browser.getControl("Remove").click()
989+
990+ >>> tag = find_tag_by_id(admin_browser.contents, 'empty')
991+ >>> print extract_text(tag.renderContents())
992+ No custom language codes have been defined.
993
994=== added file 'lib/lp/translations/templates/customlanguagecode-add.pt'
995--- lib/lp/translations/templates/customlanguagecode-add.pt 1970-01-01 00:00:00 +0000
996+++ lib/lp/translations/templates/customlanguagecode-add.pt 2009-11-19 04:05:29 +0000
997@@ -0,0 +1,29 @@
998+<html
999+ xmlns="http://www.w3.org/1999/xhtml"
1000+ xmlns:tal="http://xml.zope.org/namespaces/tal"
1001+ xmlns:metal="http://xml.zope.org/namespaces/metal"
1002+ xmlns:i18n="http://xml.zope.org/namespaces/i18n"
1003+ metal:use-macro="view/macro:page/main_only"
1004+ i18n:domain="launchpad">
1005+
1006+<body>
1007+ <div metal:fill-slot="main">
1008+ <div metal:use-macro="context/@@launchpad_form/form">
1009+ <metal:extra-info metal:fill-slot="extra_info">
1010+ <p>
1011+ Enter a language code, and select what language it should map
1012+ to during upload auto-approval.
1013+ </p>
1014+ <p>
1015+ Avoid using this capability if possible, since it makes
1016+ it harder to keep track of what goes where. For cases with
1017+ few templates, where a code would only cover one or two
1018+ translation files, it may be better to approve those
1019+ uploads manually. Launchpad will remember the filename and
1020+ approve it automatically next time it comes along.
1021+ </p>
1022+ </metal:extra-info>
1023+ </div>
1024+ </div>
1025+</body>
1026+</html>
1027
1028=== added file 'lib/lp/translations/templates/customlanguagecode-index.pt'
1029--- lib/lp/translations/templates/customlanguagecode-index.pt 1970-01-01 00:00:00 +0000
1030+++ lib/lp/translations/templates/customlanguagecode-index.pt 2009-11-19 04:05:29 +0000
1031@@ -0,0 +1,46 @@
1032+<html
1033+ xmlns="http://www.w3.org/1999/xhtml"
1034+ xmlns:tal="http://xml.zope.org/namespaces/tal"
1035+ xmlns:metal="http://xml.zope.org/namespaces/metal"
1036+ xmlns:i18n="http://xml.zope.org/namespaces/i18n"
1037+ metal:use-macro="view/macro:page/main_only"
1038+ i18n:domain="launchpad"
1039+ >
1040+<body>
1041+ <div metal:fill-slot="main">
1042+ <div class="top-portlet">
1043+ For
1044+ <tal:target replace="context/translation_target/displayname">
1045+ Evolution</tal:target>,
1046+ uploads with the language code
1047+ &ldquo;<strong tal:content="context/language_code">pt-BR</strong>&rdquo;
1048+ <tal:language condition="context/language">
1049+ are associated with the language
1050+ <a tal:replace="structure context/language/fmt:link">
1051+ Brazilian Portuguese</a>.
1052+ </tal:language>
1053+ <tal:no-language condition="not: context/language">
1054+ are ignored.
1055+ </tal:no-language>
1056+ </div>
1057+
1058+ <div class="portlet">
1059+ <ul class="horizontal">
1060+ <li tal:condition="context/required:launchpad.Admin">
1061+ <a class="remove sprite"
1062+ tal:attributes="href context/fmt:url/+remove">
1063+ remove custom language code
1064+ </a>
1065+ </li>
1066+ <li>
1067+ <a class="info sprite"
1068+ tal:attributes="href context/translation_target/fmt:url/+custom-language-codes">
1069+ custom language codes overview
1070+ </a>
1071+ </li>
1072+ </ul>
1073+ </div>
1074+ </div>
1075+</body>
1076+</html>
1077+
1078
1079=== added file 'lib/lp/translations/templates/customlanguagecodes-index.pt'
1080--- lib/lp/translations/templates/customlanguagecodes-index.pt 1970-01-01 00:00:00 +0000
1081+++ lib/lp/translations/templates/customlanguagecodes-index.pt 2009-11-19 04:05:29 +0000
1082@@ -0,0 +1,80 @@
1083+<html
1084+ xmlns="http://www.w3.org/1999/xhtml"
1085+ xmlns:tal="http://xml.zope.org/namespaces/tal"
1086+ xmlns:metal="http://xml.zope.org/namespaces/metal"
1087+ xmlns:i18n="http://xml.zope.org/namespaces/i18n"
1088+ metal:use-macro="view/macro:page/main_only"
1089+ i18n:domain="launchpad"
1090+ >
1091+<body>
1092+ <div metal:fill-slot="main">
1093+ <div class="top-portlet">
1094+ <p>
1095+ You can define custom language codes for
1096+ <tal:target replace="structure context/displayname">Evolution</tal:target>
1097+ here. Custom language codes will be treated like proper
1098+ language codes by translations imports, except each is
1099+ associated with a language you choose.
1100+ </p>
1101+ <p>
1102+ Avoid doing this if possible; it makes it harder to keep track
1103+ of what goes where during translations import, and why.
1104+ </p>
1105+ </div>
1106+ <div tal:condition="context/has_custom_language_codes"
1107+ class="portlet"
1108+ id="nonempty">
1109+ <p>
1110+ <tal:block replace="context/displayname">Evolution</tal:block>
1111+ uses the following custom language codes:
1112+ </p>
1113+ <table class="listing" style="max-width:800px">
1114+ <thead>
1115+ <tr>
1116+ <th>Code...</th>
1117+ <th>...maps to language</th>
1118+ <th tal:condition="context/required:launchpad.Admin"></th>
1119+ </tr>
1120+ </thead>
1121+ <tbody>
1122+ <tr tal:repeat="entry context/custom_language_codes">
1123+ <td align="center">
1124+ <a tal:attributes="href entry/fmt:url"
1125+ tal:content="entry/language_code">pt-PT</a>
1126+ </td>
1127+ <td>
1128+ <a tal:condition="entry/language"
1129+ tal:replace="structure entry/language/fmt:link">
1130+ Portuguese (pt)
1131+ </a>
1132+ <tal:nolanguage condition="not: entry/language">
1133+ &mdash;
1134+ </tal:nolanguage>
1135+ </td>
1136+ <td tal:condition="context/required:launchpad.Admin">
1137+ <a tal:attributes="href entry/fmt:url/+remove"
1138+ alt="Remove"
1139+ title="Remove"
1140+ class="remove sprite"></a>
1141+ </td>
1142+ </tr>
1143+ </tbody>
1144+ </table>
1145+ </div>
1146+
1147+ <p tal:condition="not: context/has_custom_language_codes"
1148+ class="portlet"
1149+ id="empty">
1150+ No custom language codes have been defined.
1151+ </p>
1152+
1153+ <div>
1154+ <a tal:attributes="href context/fmt:url/+add-custom-language-code"
1155+ tal:condition="context/required:launchpad.Admin"
1156+ class="add sprite">
1157+ Add a custom language code
1158+ </a>
1159+ </div>
1160+ </div>
1161+</body>
1162+</html>
1163
1164=== modified file 'lib/lp/translations/templates/product-portlet-translatables.pt'
1165--- lib/lp/translations/templates/product-portlet-translatables.pt 2009-10-31 12:03:43 +0000
1166+++ lib/lp/translations/templates/product-portlet-translatables.pt 2009-11-19 04:05:29 +0000
1167@@ -63,4 +63,16 @@
1168 </div>
1169
1170 </div>
1171+
1172+<div class="portlet"
1173+ tal:condition="context/required:launchpad.Admin"
1174+ id="custom-language-codes">
1175+ If necessary, you may
1176+ <a tal:attributes="href context/fmt:url/+custom-language-codes"
1177+ class="edit sprite">
1178+ define custom language codes
1179+ </a>
1180+ for this project.
1181+</div>
1182+
1183 </tal:root>
1184
1185=== modified file 'lib/lp/translations/templates/sourcepackage-translations.pt'
1186--- lib/lp/translations/templates/sourcepackage-translations.pt 2009-09-25 16:07:06 +0000
1187+++ lib/lp/translations/templates/sourcepackage-translations.pt 2009-11-19 04:05:29 +0000
1188@@ -39,6 +39,15 @@
1189 <a tal:attributes="href context/menu:navigation/download/url">
1190 download a full tarball</a> with translations.
1191 </p>
1192+ <p tal:condition="context/required:launchpad.Admin"
1193+ id="custom-language-codes">
1194+ If necessary, you may
1195+ <a tal:attributes="href context/distribution_sourcepackage/fmt:url/+custom-language-codes"
1196+ class="edit sprite">
1197+ define custom language codes
1198+ </a>
1199+ for this package.
1200+ </p>
1201 </div>
1202 </div>
1203 </div>
1204
1205=== modified file 'lib/lp/translations/tests/test_autoapproval.py'
1206--- lib/lp/translations/tests/test_autoapproval.py 2009-11-17 09:50:33 +0000
1207+++ lib/lp/translations/tests/test_autoapproval.py 2009-11-19 04:05:29 +0000
1208@@ -89,26 +89,23 @@
1209 self.assertEqual(fresh_product.getCustomLanguageCode('pt_PT'), None)
1210
1211 fresh_distro = Distribution.byName('gentoo')
1212- nocode = fresh_distro.getCustomLanguageCode(
1213- self.sourcepackagename, 'nocode')
1214+ gentoo_package = fresh_distro.getSourcePackage(self.sourcepackagename)
1215+ nocode = gentoo_package.getCustomLanguageCode('nocode')
1216 self.assertEqual(nocode, None)
1217- brazilian = fresh_distro.getCustomLanguageCode(
1218- self.sourcepackagename, 'Brazilian')
1219+ brazilian = gentoo_package.getCustomLanguageCode('Brazilian')
1220 self.assertEqual(brazilian, None)
1221
1222- fresh_package = SourcePackageName.byName('cnews')
1223- self.assertEqual(self.distro.getCustomLanguageCode(
1224- fresh_package, 'nocode'), None)
1225- self.assertEqual(self.distro.getCustomLanguageCode(
1226- fresh_package, 'Brazilian'), None)
1227+ cnews = SourcePackageName.byName('cnews')
1228+ cnews_package = self.distro.getSourcePackage(cnews)
1229+ self.assertEqual(cnews_package.getCustomLanguageCode('nocode'), None)
1230+ self.assertEqual(
1231+ cnews_package.getCustomLanguageCode('Brazilian'), None)
1232
1233 def test_UnsuccessfulCustomLanguageCodeLookup(self):
1234 # Look up nonexistent custom language code for product.
1235 self.assertEqual(self.product.getCustomLanguageCode('nocode'), None)
1236- self.assertEqual(
1237- self.distro.getCustomLanguageCode(
1238- self.sourcepackagename, 'nocode'),
1239- None)
1240+ package = self.distro.getSourcePackage(self.sourcepackagename)
1241+ self.assertEqual(package.getCustomLanguageCode('nocode'), None)
1242
1243 def test_SuccessfulProductCustomLanguageCodeLookup(self):
1244 # Look up custom language code.
1245@@ -122,8 +119,8 @@
1246
1247 def test_SuccessfulPackageCustomLanguageCodeLookup(self):
1248 # Look up custom language code.
1249- Brazilian_code = self.distro.getCustomLanguageCode(
1250- self.sourcepackagename, 'Brazilian')
1251+ package = self.distro.getSourcePackage(self.sourcepackagename)
1252+ Brazilian_code = package.getCustomLanguageCode('Brazilian')
1253 self.assertEqual(Brazilian_code, self.package_codes['Brazilian'])
1254 self.assertEqual(Brazilian_code.product, None)
1255 self.assertEqual(Brazilian_code.distribution, self.distro)