Merge lp:~danilo/launchpad/get-current-translation into lp:~launchpad/launchpad/recife

Proposed by Данило Шеган on 2010-11-15
Status: Merged
Approved by: Graham Binns on 2010-11-16
Approved revision: no longer in the source branch.
Merged at revision: 9195
Proposed branch: lp:~danilo/launchpad/get-current-translation
Merge into: lp:~launchpad/launchpad/recife
Diff against target: 314 lines (+243/-8)
4 files modified
lib/lp/translations/interfaces/potmsgset.py (+9/-0)
lib/lp/translations/model/potmsgset.py (+34/-1)
lib/lp/translations/model/side.py (+2/-6)
lib/lp/translations/tests/test_potmsgset.py (+198/-1)
To merge this branch: bzr merge lp:~danilo/launchpad/get-current-translation
Reviewer Review Type Date Requested Status
Graham Binns (community) code 2010-11-15 Approve on 2010-11-16
Review via email: mp+40885@code.launchpad.net

Description of the Change

= getCurrentTranslation =

== Background ==

This implements a getCurrentTranslation method for the upstream integration work. We are providing it in parallel to existing getCurrentTranslationMessage/getImportedTranslatioMessage methods so we make the transition easier (if we just try to replace them, a million tests fail and it's basically impossible to move from there).

The work that we are doing is enabling sharing of translations between Ubuntu and upstream projects. The concept that we introduced earlier is "translation sides" — Ubuntu or upstream — which indicate what particular translation we are interested in. So, a single POTMsgSet (basically, an English string container) will provide a translation for both an upstream project and Ubuntu package (and it can even be the same one).

This work extends our existing message sharing infrastructure, where one POTMsgSet can be in more than one POTemplate. The concepts that are relevant to this branch are "shared" and "diverged" translations. A shared translation is one that is not specific to any one POTemplate (so it has TranslationMessage.potemplate == None), and as such is shared between any POTemplates which have no diverged translation. A diverged translation is a TranslationMessage linked to a POTMsgSet which has POTemplate set, and is an overriding translation strictly for that POTemplate.

== Implementation ==

IPOTMsgSet.getCurrentTranslation gets a current translation for POTMsgSet
for a given POTemplate and a given Language. By default, it assumes you are looking for whatever side POTemplate is on (either upstream or Ubuntu)

When POTemplate is not passed in, it is treated as a request solely for the shared message.

TranslationSide traits class is used to generalize some common operations which depend on the side (UPSTREAM/UBUNTU) without having to write code that specifically calls that out. For instance, getFlag call that is used on TranslationMessage is used to construct "TranslationMessage.is_current_ubuntu == True" or "TranslationMessage.is_current_upstream == True" Storm clauses.

However, that gives me a security proxied object so I had to add a removeSecurityProxy around it. I am not sure how I could better solve this (other than moving it into the traits implementation itself, but that's probably even worse). Any suggestions welcome.

Rest of it should be fairly self-explanatory, except perhaps for the order_by clause at the end: it simply sorts diverged translations (those with POTemplate set) before shared ones (so they are preferred ones).

Tests are unified for both "sides" and only basic methods are re-implemented. If test set-up is not clear, I'd be happy to re-write it and provide better commentary.

== Demo & QA ==

This goes into our integration branch, and will be QAd together with it. At the moment, this method is not used and that's coming in follow-up branches: we are taking it step-by-step.

== Tests ==

bin/test -cvvt GetCurrentTranslation

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/translations/interfaces/potmsgset.py
  lib/lp/translations/model/potmsgset.py
  lib/lp/translations/tests/test_potmsgset.py

To post a comment you must log in.
Jeroen T. Vermeulen (jtv) wrote :

Doesn't this duplicate TranslationSideTraits.getCurrentMessage and POTMsgSet._getUsedTranslationMessage? If so, you could use this new code to replace those (and conveniently sweep out any non-Storm code along the way). That would require splitting it up a bit further though.

Also, I question the wisdom of intelligently deriving the translation side from a potemplate argument that is optional. We're using the same argument for two very different purposes: divergence and side-sensitivity. It gives rise to two kinds of call: "figure out the side from this template" and "I'll figure out the side; the template may be None." We'd be better off with those as separate methods to represent side-sensitive and side-insensitive "layers" in our code. The latter is basically our existing get{Current,Imported}TranslationMessage and the former is basically a wrapper around that layer.

To do case-sensitivity right we'd need another optional argument, along the lines of ignore_diverged=False. I think that would make the interfaces cleaner and more helpful overall.

Jeroen

Jeroen T. Vermeulen (jtv) wrote :

(By the way the branch I'm working on has a basic 3-line stub implementation of getCurrentTranslation, with the potemplate non-optional; supporting "ignore_diverged" would only take an extra "if ignore_diverged: potemplate = None" after the TranslationSideTraits have been retrieved.)

Данило Шеган (danilo) wrote :

Hi Jeroen,

Thanks for taking a look.

У уто, 16. 11 2010. у 07:16 +0000, Jeroen T. Vermeulen пише:
> Doesn't this duplicate TranslationSideTraits.getCurrentMessage and
> POTMsgSet._getUsedTranslationMessage?

Uhm, yes. That's the whole idea: create new methods which support the
new semantics (concept of sides), while keeping old ones intact.

> If so, you could use this new code to replace those (and conveniently
> sweep out any non-Storm code along the way).

Ultimately, yes. The branch's sole purpose is to not remove/replace any
existing code because that's going to cause a lot of test breakage.
I've even tried that long time ago and decided to split it up for that
very reason. I think it was even you who strongly pushed for the
approach, so I am very much surprised by these questions. :)

FWIW, TranslationSideTraits.getCurrentMessage should be switched to use
this method or the other way around. Not in this branch, though.

> That would require splitting it up a bit further though.

This originally started as an internal method to replace _getUsedTM. As
soon as we decide to switch actual callsites (wrapper methods in
IPOTMsgSet) to the new method, I believe it should be made private
again. Or perhaps not, and we should just use this single method.

> Also, I question the wisdom of intelligently deriving the translation
> side from a potemplate argument that is optional. We're using the
> same argument for two very different purposes: divergence and
> side-sensitivity.

Yes, I did consider that too. It turned out it's useful and simplifies
testing. It's easy to split that code out, but as we start using this
method in tests more and more, we are going to hate to have to pass in
potemplate.translation_side in every single time. Perhaps that should
live in the test helpers, and I am fine with that if that's what you'd
agree with.

Still, I feel it's not heavy at all and could very well stay in, but I
am not very attached to the functionality. ;)

> It gives rise to two kinds of call: "figure out the
> side from this template" and "I'll figure out the side; the template
> may be None." We'd be better off with those as separate methods to
> represent side-sensitive and side-insensitive "layers" in our code.
> The latter is basically our existing
> get{Current,Imported}TranslationMessage and the former is basically a
> wrapper around that layer.
>
> To do case-sensitivity right we'd need another optional argument,
> along the lines of ignore_diverged=False. I think that would make the
> interfaces cleaner and more helpful overall.

Uhm, not really (on the "we'd need"). You can get a shared translation
for either side by passing the side explicitely in (while keeping
potemplate=None). As I said, this method could probably be made private
instead.

Данило Шеган (danilo) wrote :

Ok, Jeroen and I discussed this on IRC in order to agree on a general approach (and clarify what I've been doing). I've done a few minor changes and am now looking for a proper review :)

Graham Binns (gmb) :
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/translations/interfaces/potmsgset.py'
2--- lib/lp/translations/interfaces/potmsgset.py 2010-11-04 09:32:28 +0000
3+++ lib/lp/translations/interfaces/potmsgset.py 2010-11-16 10:51:59 +0000
4@@ -279,6 +279,15 @@
5 If a translation conflict is detected, TranslationConflict is raised.
6 """
7
8+ def getCurrentTranslation(potemplate, language, side=None):
9+ """Get a current translation message.
10+
11+ :param potemplate: An `IPOTemplate` to look up a translation for.
12+ If it's None, returns a shared translation.
13+ :param language: translation should be to this `ILanguage`.
14+ :param side: translation side to look at.
15+ """
16+
17 def setCurrentTranslation(pofile, submitter, translations, origin,
18 share_with_other_side=False,
19 lock_timestamp=None):
20
21=== modified file 'lib/lp/translations/model/potmsgset.py'
22--- lib/lp/translations/model/potmsgset.py 2010-11-15 16:25:05 +0000
23+++ lib/lp/translations/model/potmsgset.py 2010-11-16 10:51:59 +0000
24@@ -20,7 +20,12 @@
25 SQLObjectNotFound,
26 StringCol,
27 )
28-from storm.expr import SQL
29+from storm.expr import (
30+ Coalesce,
31+ Desc,
32+ Or,
33+ SQL,
34+ )
35 from storm.store import (
36 EmptyResultSet,
37 Store,
38@@ -327,6 +332,34 @@
39 return self._getUsedTranslationMessage(
40 None, language, current=True)
41
42+ def getCurrentTranslation(self, potemplate, language, side):
43+ """See `IPOTMsgSet`."""
44+
45+ from zope.security.proxy import removeSecurityProxy
46+ assert side is not None, "Translation side must be specified."
47+
48+ traits = getUtility(ITranslationSideTraitsSet).getTraits(side)
49+ clauses = [removeSecurityProxy(
50+ traits.getFlag(TranslationMessage)) == True]
51+
52+ if potemplate is None:
53+ # Look only for a shared translation.
54+ clauses.append(TranslationMessage.potemplate == None)
55+ else:
56+ clauses.append(
57+ Or(TranslationMessage.potemplate == None,
58+ TranslationMessage.potemplateID == potemplate.id))
59+ clauses.extend([
60+ TranslationMessage.potmsgsetID == self.id,
61+ TranslationMessage.languageID == language.id])
62+
63+ # Return a diverged translation if it exists, and fall back
64+ # to the shared one otherwise.
65+ result = Store.of(self).find(
66+ TranslationMessage, *clauses).order_by(
67+ Desc(Coalesce(TranslationMessage.potemplateID, -1))).first()
68+ return result
69+
70 def getLocalTranslationMessages(self, potemplate, language,
71 include_dismissed=False,
72 include_unreviewed=True):
73
74=== modified file 'lib/lp/translations/model/side.py'
75--- lib/lp/translations/model/side.py 2010-08-23 08:41:03 +0000
76+++ lib/lp/translations/model/side.py 2010-11-16 10:51:59 +0000
77@@ -33,12 +33,8 @@
78
79 def getCurrentMessage(self, potmsgset, potemplate, language):
80 """See `ITranslationSideTraits`."""
81- if self.side == TranslationSide.UPSTREAM:
82- return potmsgset.getImportedTranslationMessage(
83- potemplate, language)
84- else:
85- return potmsgset.getCurrentTranslationMessage(
86- potemplate, language)
87+ return potmsgset.getCurrentTranslation(
88+ potemplate, language, self.side)
89
90 def setFlag(self, translationmessage, value):
91 """See `ITranslationSideTraits`."""
92
93=== modified file 'lib/lp/translations/tests/test_potmsgset.py'
94--- lib/lp/translations/tests/test_potmsgset.py 2010-11-04 09:32:28 +0000
95+++ lib/lp/translations/tests/test_potmsgset.py 2010-11-16 10:51:59 +0000
96@@ -24,7 +24,6 @@
97 from lp.registry.interfaces.person import IPersonSet
98 from lp.registry.interfaces.product import IProductSet
99 from lp.testing import TestCaseWithFactory
100-from lp.services.worlddata.interfaces.language import ILanguageSet
101 from lp.translations.interfaces.potemplate import IPOTemplateSet
102 from lp.translations.interfaces.potmsgset import (
103 POTMsgSetInIncompatibleTemplatesError,
104@@ -38,6 +37,7 @@
105 RosettaTranslationOrigin,
106 TranslationConflict,
107 )
108+from lp.translations.interfaces.side import TranslationSide
109 from lp.translations.model.translationmessage import DummyTranslationMessage
110
111
112@@ -1713,6 +1713,203 @@
113 lock_timestamp=lock_timestamp)
114
115
116+class BaseTestGetCurrentTranslation(object):
117+ layer = DatabaseFunctionalLayer
118+
119+ def test_no_translation(self):
120+ # getCurrentTranslation returns None when there's no translation.
121+ pofile, potmsgset = self._makePOFileAndPOTMsgSet()
122+
123+ self.assertIs(None,
124+ potmsgset.getCurrentTranslation(
125+ pofile.potemplate, pofile.language, self.this_side))
126+
127+ def test_basic_get(self):
128+ # getCurrentTranslation gets the current translation
129+ # for a message.
130+ pofile, potmsgset = self._makePOFileAndPOTMsgSet()
131+ translations = {
132+ 0: self.factory.getUniqueString('translation'), }
133+ origin = RosettaTranslationOrigin.SCM
134+ message = potmsgset.setCurrentTranslation(
135+ pofile, pofile.potemplate.owner, translations, origin)
136+
137+ self.assertEqual(message,
138+ potmsgset.getCurrentTranslation(
139+ pofile.potemplate, pofile.language,
140+ self.this_side))
141+
142+ def test_assertion_on_bad_parameters(self):
143+ # When no potemplate is passed in to getCurrentTranslation,
144+ # and no explicit side is selected, AssertionError is raised.
145+ pofile, potmsgset = self._makePOFileAndPOTMsgSet()
146+
147+ self.assertRaises(AssertionError,
148+ potmsgset.getCurrentTranslation,
149+ None, pofile.language, None)
150+
151+ def test_other_languages_ignored(self):
152+ # getCurrentTranslation never returns a translation for another
153+ # language.
154+ pofile, potmsgset = self._makePOFileAndPOTMsgSet()
155+ pofile_other_language = self.factory.makePOFile(
156+ potemplate=pofile.potemplate)
157+ translations = {
158+ 0: self.factory.getUniqueString('translation'), }
159+ origin = RosettaTranslationOrigin.SCM
160+ message = potmsgset.setCurrentTranslation(
161+ pofile_other_language, pofile.potemplate.owner,
162+ translations, origin)
163+
164+ self.assertIs(None,
165+ potmsgset.getCurrentTranslation(
166+ pofile.potemplate, pofile.language, self.this_side))
167+
168+ def test_other_diverged_no_translation(self):
169+ # getCurrentTranslation gets the current upstream translation
170+ # for a message.
171+ pofile, potmsgset = self._makePOFileAndPOTMsgSet()
172+ pofile_other = self._makeOtherPOFile(pofile, potmsgset)
173+
174+ # Create a diverged translation in pofile_other.
175+ translations = {
176+ 0: self.factory.getUniqueString('translation'), }
177+ suggestion = potmsgset.submitSuggestion(
178+ pofile_other, pofile_other.potemplate.owner, translations)
179+ suggestion.approveAsDiverged(
180+ pofile_other, pofile_other.potemplate.owner)
181+
182+ self.assertIs(None,
183+ potmsgset.getCurrentTranslation(
184+ pofile.potemplate, pofile.language, self.this_side))
185+
186+ def test_other_side(self):
187+ # getCurrentTranslation gets the current translation
188+ # for a message depending on the side that is specified.
189+ pofile, potmsgset = self._makePOFileAndPOTMsgSet()
190+ pofile_other = self._makeOtherPOFile(pofile, potmsgset)
191+
192+ # Create current translations in 'pofile' and 'pofile_other'.
193+ translations_here = {
194+ 0: self.factory.getUniqueString('here'), }
195+ translations_other = {
196+ 0: self.factory.getUniqueString('other'), }
197+ origin = RosettaTranslationOrigin.SCM
198+
199+ current_translation = potmsgset.setCurrentTranslation(
200+ pofile, pofile.potemplate.owner,
201+ translations_here, origin)
202+ other_translation = potmsgset.setCurrentTranslation(
203+ pofile_other, pofile_other.potemplate.owner,
204+ translations_other, origin)
205+
206+ self.assertEquals(current_translation,
207+ potmsgset.getCurrentTranslation(
208+ pofile_other.potemplate, pofile_other.language,
209+ self.this_side))
210+ self.assertEquals(other_translation,
211+ potmsgset.getCurrentTranslation(
212+ pofile.potemplate, pofile.language,
213+ self.other_side))
214+
215+ def test_prefers_diverged(self):
216+ # getCurrentTranslation prefers a diverged translation if
217+ # it's available for the given potemplate.
218+ pofile, potmsgset = self._makePOFileAndPOTMsgSet()
219+
220+ # Create both a shared and a diverged translation in pofile.
221+ translations_shared = {
222+ 0: self.factory.getUniqueString('shared'), }
223+ translations_diverged = {
224+ 0: self.factory.getUniqueString('diverged'), }
225+ origin = RosettaTranslationOrigin.SCM
226+ shared_message = potmsgset.setCurrentTranslation(
227+ pofile, pofile.potemplate.owner, translations_shared, origin)
228+ diverged_message = potmsgset.submitSuggestion(
229+ pofile, pofile.potemplate.owner, translations_diverged)
230+ diverged_message.approveAsDiverged(pofile, pofile.potemplate.owner)
231+
232+ self.assertEquals(diverged_message,
233+ potmsgset.getCurrentTranslation(
234+ pofile.potemplate, pofile.language,
235+ self.this_side))
236+
237+ def test_shared_when_requested(self):
238+ # getCurrentTranslation returns a shared translation even with
239+ # diverged translation present if shared one was asked for.
240+ pofile, potmsgset = self._makePOFileAndPOTMsgSet()
241+
242+ # Create both a shared and a diverged translation in pofile.
243+ translations_shared = {
244+ 0: self.factory.getUniqueString('shared'), }
245+ translations_diverged = {
246+ 0: self.factory.getUniqueString('diverged'), }
247+ origin = RosettaTranslationOrigin.SCM
248+ shared_message = potmsgset.setCurrentTranslation(
249+ pofile, pofile.potemplate.owner, translations_shared, origin)
250+ diverged_message = potmsgset.submitSuggestion(
251+ pofile, pofile.potemplate.owner, translations_diverged)
252+ diverged_message.approveAsDiverged(pofile, pofile.potemplate.owner)
253+
254+ self.assertEquals(shared_message,
255+ potmsgset.getCurrentTranslation(
256+ None, pofile.language, self.this_side))
257+
258+
259+class TestGetCurrentTranslationForUpstreams(BaseTestGetCurrentTranslation,
260+ TestCaseWithFactory):
261+ """getCurrentTranslation working on an upstream POFile."""
262+
263+ def setUp(self):
264+ super(TestGetCurrentTranslationForUpstreams, self).setUp(
265+ 'carlos@canonical.com')
266+ self.this_side = TranslationSide.UPSTREAM
267+ self.other_side = TranslationSide.UBUNTU
268+
269+ def _makePOFileAndPOTMsgSet(self):
270+ pofile = self.factory.makePOFile()
271+ potmsgset = self.factory.makePOTMsgSet(pofile.potemplate)
272+ return pofile, potmsgset
273+
274+ def _makeOtherPOFile(self, pofile, potmsgset):
275+ sp = self.factory.makeSourcePackage()
276+ potemplate = self.factory.makePOTemplate(
277+ name=pofile.potemplate.name,
278+ distroseries=sp.distroseries,
279+ sourcepackagename=sp.sourcepackagename)
280+ pofile_other = self.factory.makePOFile(potemplate=potemplate,
281+ language=pofile.language)
282+ potmsgset.setSequence(potemplate, 1)
283+ return pofile_other
284+
285+
286+class TestGetCurrentTranslationForUbuntu(BaseTestGetCurrentTranslation,
287+ TestCaseWithFactory):
288+ """getCurrentTranslation working on an Ubuntu POFile."""
289+
290+ def setUp(self):
291+ super(TestGetCurrentTranslationForUbuntu, self).setUp(
292+ 'carlos@canonical.com')
293+ self.this_side = TranslationSide.UBUNTU
294+ self.other_side = TranslationSide.UPSTREAM
295+
296+ def _makePOFileAndPOTMsgSet(self):
297+ sp = self.factory.makeSourcePackage()
298+ potemplate = self.factory.makePOTemplate(
299+ distroseries=sp.distroseries,
300+ sourcepackagename=sp.sourcepackagename)
301+ pofile = self.factory.makePOFile(potemplate=potemplate)
302+ potmsgset = self.factory.makePOTMsgSet(pofile.potemplate)
303+ return pofile, potmsgset
304+
305+ def _makeOtherPOFile(self, pofile, potmsgset):
306+ potemplate = self.factory.makePOTemplate(name=pofile.potemplate.name)
307+ pofile_other = self.factory.makePOFile(potemplate=potemplate,
308+ language=pofile.language)
309+ potmsgset.setSequence(potemplate, 1)
310+ return pofile_other
311+
312+
313 class TestCheckForConflict(TestCaseWithFactory):
314 """Test POTMsgSet._checkForConflict."""
315

Subscribers

People subscribed via source and target branches

to all changes: