Merge lp:~henninge/launchpad/devel-710591-setcurrenttranslation-extension into lp:launchpad

Proposed by Henning Eggers
Status: Merged
Approved by: Henning Eggers
Approved revision: no longer in the source branch.
Merged at revision: 12337
Proposed branch: lp:~henninge/launchpad/devel-710591-setcurrenttranslation-extension
Merge into: lp:launchpad
Prerequisite: lp:~henninge/launchpad/devel-710591-sharing-info-groundwork-0
Diff against target: 373 lines (+289/-4)
5 files modified
lib/lp/testing/factory.py (+3/-3)
lib/lp/translations/interfaces/translationmessage.py (+11/-0)
lib/lp/translations/model/potmsgset.py (+73/-0)
lib/lp/translations/model/translationmessage.py (+23/-1)
lib/lp/translations/tests/test_translationmessage.py (+179/-0)
To merge this branch: bzr merge lp:~henninge/launchpad/devel-710591-setcurrenttranslation-extension
Reviewer Review Type Date Requested Status
Ian Booth (community) release-critical Approve
Jeroen T. Vermeulen (community) code Approve
Review via email: mp+48594@code.launchpad.net

Commit message

[r=jtv][ui=none] [r=jtv][bug=710591][incr] Provide TranslationMessage.acceptFromImport and .acceptFromUpstreamImportOnPackage.

Description of the change

= Summary =

Provide a method to import translations in old style. Old style means that
translations coming from an import are marked as "imported" by setting the
"current" flag on the other side. It also follow the precedence rules as
outlined here:
https://wiki.ubuntu.com/Translations/KnowledgeBase/TranslationsPrecedence

== Proposed fix ==

Provide a method "TranslationMessage.acceptAsImported" to be used instead of
"TranslationMessage.approve" during import. Using the "approve" method here
is misleading as translations are not really approved. Hence the new method
does not take a "reviewer" parameter and does not set the "reviewer" and
"date_reviewed" attributes of a TranslationMessage.

== Pre-implementation notes ==

Talked to danilo about the need to fix this as a special case for
setCurrentTranslation.

== Implementation details ==

== Tests ==

bin/test -vvcm lp.translations.tests.test_translationmessage \
    -t AcceptAsImported

== Demo and Q/A ==

None.

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/translations/interfaces/translationmessage.py
  lib/lp/translations/tests/test_translationmessage.py
  lib/lp/testing/factory.py
  lib/lp/translations/model/potmsgset.py
  lib/lp/translations/model/translationmessage.py

To post a comment you must log in.
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

(Typo in ITranslation.acceptAsImported: "sold_style_import")

Hi Henning,

This looks like a very nice branch overall. Well-tested, well-documented, simple solution to a complex problem. We discussed on IRC, but unfortunately I don't have time to continue doing that today. The branch looks good to me in essence, so I'm approving now but with a few conditions.

First of those: POTMsgSet.acceptAsImported is two methods dressed up as one. There's a "mode switch" that I expect is going to be set explicitly near the call site, and then the two paths inside the method have very little in common. I can see how the method may have grown in that way, but now that it's stabilized it's clear that it needs to be split into two.

By the way, you note that the "old-style import" will be used "only if the upload is by_maintainer and to a source package and no upstream template [exists]." That means that the old-style code probably doesn't need to abstract away the two translation sides. That seems like a very specific behaviour involving both sides, in an asymmetric way. So perhaps the reasoning becomes easier if the sides are kept concrete. But I'll leave that for your consideration.

Second condition: "old-style" isn't going to be very meaningful to someone who started or will start working on Translations since the Recife rollout. That needs a better term. The way you describe it is nice and clear to me, but probably not to someone without the Rosetta background. Future engineers who read that may be afraid to touch the code (because they don't know what the old style does and why) or remove it (because you make it sound like something that's been dropped from the design).

We discussed this on IRC. It's hard to come up with a good name, but how about "upstream_for_package"?

Jeroen

review: Approve (code)
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

Oh, another note:

184 + def test_accept_marks_not_reviewed(self):
185 + # Accepting a suggestion does not update its review fields.

The name and the comment describe different behaviours. I believe the comment is the correct one. Could you fix the name?

Jeroen

Revision history for this message
Henning Eggers (henninge) wrote :

Thank you for the review. I split the method into two called "acceptFromImport" and "acceptFromUpstreamImportOnPackage", this way it is clear that this is a special case and the "old style" is gone, too. I also applied your suggestion from IRC to use the explicit flag names and added an assert that acceptFromUpstreamImportOnPackage can only be used on a source package.

I fail to produce an incremental diff as I had to merge devel to get some helpful factory additions that Aaron did. He added a "side" parameter to makePOTemplate and makePOFile so that it now much easier to test with source package templates.

Revision history for this message
Henning Eggers (henninge) wrote :

Hi Ian,
this was supposed to make the deadline but ec2 failed because of some silly little oversight. This the second-to-last branch in a series to fix bug 710591 which is Critical.

Revision history for this message
Ian Booth (wallyworld) wrote :

Hi

One very minor point: the sentence

"This method allow to store translation as upstream translation
even though there is no upstream template"

doesn't quite make sense as written. Perhaps it could be corrected.

review: Approve (release-critical)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/testing/factory.py'
2--- lib/lp/testing/factory.py 2011-02-03 19:11:38 +0000
3+++ lib/lp/testing/factory.py 2011-02-05 15:31:44 +0000
4@@ -2716,10 +2716,10 @@
5 removeSecurityProxy(potmsgset).sync()
6 return potmsgset
7
8- def makePOFileAndPOTMsgSet(self, language_code, msgid=None,
9- with_plural=False):
10+ def makePOFileAndPOTMsgSet(self, language_code=None, msgid=None,
11+ with_plural=False, side=None):
12 """Make a `POFile` with a `POTMsgSet`."""
13- pofile = self.makePOFile(language_code)
14+ pofile = self.makePOFile(language_code, side=side)
15
16 if with_plural:
17 if msgid is None:
18
19=== modified file 'lib/lp/translations/interfaces/translationmessage.py'
20--- lib/lp/translations/interfaces/translationmessage.py 2011-01-31 16:02:10 +0000
21+++ lib/lp/translations/interfaces/translationmessage.py 2011-02-05 15:31:44 +0000
22@@ -267,6 +267,17 @@
23 def approveAsDiverged(pofile, reviewer, lock_timestamp=None):
24 """Approve this suggestion, as a diverged translation."""
25
26+ def acceptFromImport(pofile, share_with_other_side=False,
27+ lock_timestamp=None):
28+ """Accept a suggestion coming from a translation import."""
29+
30+ def acceptFromUpstreamImportOnPackage(pofile, lock_timestamp=None):
31+ """Accept a suggestion coming from a translation import.
32+
33+ This method allows to mark a translation as being current in
34+ upstream even though there is no upstream template.
35+ """
36+
37 # XXX CarlosPerelloMarin 20071022: We should move this into browser code.
38 def makeHTMLID(description):
39 """Unique identifier for self, suitable for use in HTML element ids.
40
41=== modified file 'lib/lp/translations/model/potmsgset.py'
42--- lib/lp/translations/model/potmsgset.py 2011-01-28 01:08:34 +0000
43+++ lib/lp/translations/model/potmsgset.py 2011-02-05 15:31:44 +0000
44@@ -739,6 +739,79 @@
45 template.awardKarma(translator, 'translationsuggestionapproved')
46 template.awardKarma(reviewer, 'translationreview')
47
48+ def acceptFromImport(self, pofile, suggestion,
49+ share_with_other_side=False, lock_timestamp=None):
50+ """Accept a suggestion coming from a translation import.
51+
52+ When importing translations, these are first added as a suggestion
53+ and only after successful validation they are made current. This is
54+ slightly different to approving a suggestion because no reviewer is
55+ credited.
56+
57+ :param pofile: The `POFile` that the suggestion is being approved for.
58+ :param suggestion: The `TranslationMessage` being approved.
59+ :param share_with_other_side: Policy selector: share this change with
60+ the other translation side if possible?
61+ :param lock_timestamp: Timestamp of the original translation state
62+ that this change is based on.
63+ """
64+ template = pofile.potemplate
65+ traits = getUtility(ITranslationSideTraitsSet).getTraits(
66+ template.translation_side)
67+ if traits.getFlag(suggestion):
68+ # Message is already current.
69+ return
70+
71+ translator = suggestion.submitter
72+ potranslations = dictify_translations(suggestion.all_msgstrs)
73+ self._setTranslation(
74+ pofile, translator, suggestion.origin, potranslations,
75+ share_with_other_side=share_with_other_side,
76+ identical_message=suggestion, lock_timestamp=lock_timestamp)
77+
78+ def acceptFromUpstreamImportOnPackage(self, pofile, suggestion,
79+ lock_timestamp=None):
80+ """Accept a suggestion coming from a translation import.
81+
82+ This method allow to store translation as upstream translation
83+ even though there is no upstream template. It is similar to
84+ acceptFromImport but will make sure not to overwrite existing
85+ translations. Rather, it will mark the translation as being current
86+ in upstream.
87+
88+ :param pofile: The `POFile` that the suggestion is being approved for.
89+ :param suggestion: The `TranslationMessage` being approved.
90+ :param lock_timestamp: Timestamp of the original translation state
91+ that this change is based on.
92+ """
93+ template = pofile.potemplate
94+ assert template.translation_side == TranslationSide.UBUNTU, (
95+ "Do not use this method for an upstream project.")
96+
97+ if suggestion.is_current_ubuntu and suggestion.is_current_upstream:
98+ # Message is already current.
99+ return
100+
101+ current = self.getCurrentTranslation(
102+ template, pofile.language, template.translation_side)
103+ other = self.getOtherTranslation(
104+ pofile.language, template.translation_side)
105+ if other is not None:
106+ other.is_current_upstream = False
107+ if current is None or other is None:
108+ translator = suggestion.submitter
109+ potranslations = dictify_translations(suggestion.all_msgstrs)
110+ self._setTranslation(
111+ pofile, translator, suggestion.origin, potranslations,
112+ share_with_other_side=True,
113+ identical_message=suggestion,
114+ lock_timestamp=lock_timestamp)
115+ else:
116+ # Make it only current in upstream.
117+ suggestion.is_current_upstream = True
118+ if suggestion != other:
119+ pofile.markChanged(translator=suggestion.submitter)
120+
121 def _cloneAndDiverge(self, original_message, pofile):
122 """Create a diverged clone of a `TranslationMessage`.
123
124
125=== modified file 'lib/lp/translations/model/translationmessage.py'
126--- lib/lp/translations/model/translationmessage.py 2011-01-31 19:15:50 +0000
127+++ lib/lp/translations/model/translationmessage.py 2011-02-05 15:31:44 +0000
128@@ -172,6 +172,14 @@
129 """See `ITranslationMessage`."""
130 raise NotImplementedError()
131
132+ def acceptFromImport(self, *args, **kwargs):
133+ """See `ITranslationMessage`."""
134+ raise NotImplementedError()
135+
136+ def acceptFromUpstreamImportOnPackage(self, *args, **kwargs):
137+ """See `ITranslationMessage`."""
138+ raise NotImplementedError()
139+
140 def getOnePOFile(self):
141 """See `ITranslationMessage`."""
142 return None
143@@ -343,6 +351,19 @@
144 return self.potmsgset.approveAsDiverged(
145 pofile, self, reviewer, lock_timestamp=lock_timestamp)
146
147+ def acceptFromImport(self, pofile, share_with_other_side=False,
148+ lock_timestamp=None):
149+ """See `ITranslationMessage`."""
150+ self.potmsgset.acceptFromImport(
151+ pofile, self, share_with_other_side=share_with_other_side,
152+ lock_timestamp=lock_timestamp)
153+
154+ def acceptFromUpstreamImportOnPackage(self, pofile,
155+ lock_timestamp=None):
156+ """See `ITranslationMessage`."""
157+ self.potmsgset.acceptFromUpstreamImportOnPackage(
158+ pofile, self, lock_timestamp=lock_timestamp)
159+
160 def getOnePOFile(self):
161 """See `ITranslationMessage`."""
162 from lp.translations.model.pofile import POFile
163@@ -392,12 +413,13 @@
164
165 def shareIfPossible(self):
166 """See `ITranslationMessage`."""
167- if not self.is_diverged:
168+ if self.potemplate is None:
169 # Already converged.
170 return
171
172 # Existing shared direct equivalent to this message, if any.
173 shared = self.getSharedEquivalent()
174+
175 # Existing shared ubuntu translation for this POTMsgSet, if
176 # any.
177 ubuntu = self.potmsgset.getCurrentTranslation(
178
179=== modified file 'lib/lp/translations/tests/test_translationmessage.py'
180--- lib/lp/translations/tests/test_translationmessage.py 2011-02-03 17:38:15 +0000
181+++ lib/lp/translations/tests/test_translationmessage.py 2011-02-05 15:31:44 +0000
182@@ -77,6 +77,8 @@
183
184
185 class TestApprove(TestCaseWithFactory):
186+ """Tests for `TranslationMessage.approve`."""
187+
188 layer = ZopelessDatabaseLayer
189
190 def test_approve_activates_message(self):
191@@ -294,6 +296,183 @@
192 self.assertNotEqual(ubuntu_tm, upstream_tm)
193
194
195+class TestAcceptFromImport(TestCaseWithFactory):
196+ """Tests for `TranslationMessage.acceptFromImport`.
197+
198+ This method is a lot like `TranslationMessage.approve`, so this test
199+ mainly exercises what it does differently.
200+ """
201+
202+ layer = ZopelessDatabaseLayer
203+
204+ def test_accept_activates_message(self):
205+ # An untranslated message receives an imported translation.
206+ pofile = self.factory.makePOFile()
207+ suggestion = self.factory.makeSuggestion(pofile=pofile)
208+ reviewer = self.factory.makePerson()
209+ self.assertFalse(suggestion.is_current_upstream)
210+ self.assertFalse(suggestion.is_current_ubuntu)
211+
212+ suggestion.acceptFromImport(pofile)
213+
214+ # By default the suggestion becomes current only on the side
215+ # (upstream or Ubuntu) that it's being approved on.
216+ self.assertTrue(suggestion.is_current_upstream)
217+ self.assertFalse(suggestion.is_current_ubuntu)
218+
219+ def test_accept_can_make_other_side_track(self):
220+ # In some situations (see POTMsgSet.setCurrentTranslation for
221+ # details) the acceptance can be made to propagate to the other
222+ # side, subject to the share_with_other_side parameter.
223+ pofile = self.factory.makePOFile()
224+ suggestion = self.factory.makeSuggestion(pofile=pofile)
225+ reviewer = self.factory.makePerson()
226+
227+ self.assertFalse(suggestion.is_current_upstream)
228+ self.assertFalse(suggestion.is_current_ubuntu)
229+
230+ suggestion.acceptFromImport(pofile, share_with_other_side=True)
231+
232+ self.assertTrue(suggestion.is_current_upstream)
233+ self.assertTrue(suggestion.is_current_ubuntu)
234+
235+ def test_accept_does_not_set_review_fields(self):
236+ # Accepting a suggestion does not update its review fields.
237+ pofile = self.factory.makePOFile()
238+ suggestion = self.factory.makeSuggestion(pofile=pofile)
239+ reviewer = self.factory.makePerson()
240+
241+ self.assertIs(None, suggestion.reviewer)
242+ self.assertIs(None, suggestion.date_reviewed)
243+
244+ suggestion.acceptFromImport(pofile)
245+
246+ self.assertIs(None, suggestion.reviewer)
247+ self.assertIs(None, suggestion.date_reviewed)
248+
249+ def test_accept_awards_no_karma(self):
250+ # The translator receives no karma.
251+ translator = self.factory.makePerson()
252+ pofile = self.factory.makePOFile()
253+ suggestion = self.factory.makeSuggestion(
254+ pofile=pofile, translator=translator)
255+
256+ karmarecorder = self.installKarmaRecorder(person=translator)
257+ suggestion.acceptFromImport(pofile)
258+
259+ self.assertEqual([], karmarecorder.karma_events)
260+
261+class TestAcceptFromUpstreamImportOnPackage(TestCaseWithFactory):
262+ """Tests for `TranslationMessage.acceptFromUpstreamImportOnPackage`.
263+
264+ This method is a lot like `TranslationMessage.acceptFromImport`, so this
265+ test mainly exercises what it does differently.
266+ """
267+
268+ layer = ZopelessDatabaseLayer
269+
270+ def test_accept_activates_message_if_untranslated(self):
271+ # An untranslated message accepts an imported translation.
272+ pofile = self.factory.makePOFile(side=TranslationSide.UBUNTU)
273+ suggestion = self.factory.makeSuggestion(pofile=pofile)
274+ reviewer = self.factory.makePerson()
275+ self.assertFalse(suggestion.is_current_ubuntu)
276+ self.assertFalse(suggestion.is_current_upstream)
277+
278+ suggestion.acceptFromUpstreamImportOnPackage(pofile)
279+
280+ # Messages are always accepted on the other side, too.
281+ self.assertTrue(suggestion.is_current_ubuntu)
282+ self.assertTrue(suggestion.is_current_upstream)
283+
284+ def test_accept_no_previously_imported(self):
285+ # If there was already a current translation, but no previously
286+ # imported one, it is disabled when a suggestion is accepted.
287+ pofile, potmsgset = self.factory.makePOFileAndPOTMsgSet(
288+ side=TranslationSide.UBUNTU)
289+ suggestion = self.factory.makeSuggestion(
290+ pofile=pofile, potmsgset=potmsgset)
291+ incumbent_message = self.factory.makeCurrentTranslationMessage(
292+ pofile=pofile, potmsgset=potmsgset)
293+
294+ self.assertTrue(incumbent_message.is_current_ubuntu)
295+ self.assertFalse(suggestion.is_current_ubuntu)
296+
297+ suggestion.acceptFromUpstreamImportOnPackage(pofile)
298+
299+ self.assertFalse(incumbent_message.is_current_ubuntu)
300+ self.assertTrue(suggestion.is_current_ubuntu)
301+ # Messages are always accepted on the other side, too.
302+ self.assertTrue(suggestion.is_current_upstream)
303+
304+ def test_accept_previously_imported(self):
305+ # If there was already a current translation, and a previously
306+ # imported one, the current translation is left untouched.
307+ pofile, potmsgset = self.factory.makePOFileAndPOTMsgSet(
308+ side=TranslationSide.UBUNTU)
309+ imported_message = self.factory.makeCurrentTranslationMessage(
310+ pofile=pofile, potmsgset=potmsgset, current_other=True)
311+ imported_message.is_current_ubuntu = False
312+
313+ suggestion = self.factory.makeSuggestion(
314+ pofile=pofile, potmsgset=potmsgset)
315+ incumbent_message = self.factory.makeCurrentTranslationMessage(
316+ pofile=pofile, potmsgset=potmsgset)
317+
318+ self.assertTrue(incumbent_message.is_current_ubuntu)
319+ self.assertFalse(suggestion.is_current_ubuntu)
320+ self.assertTrue(imported_message.is_current_upstream)
321+
322+ suggestion.acceptFromUpstreamImportOnPackage(pofile)
323+
324+ self.assertTrue(incumbent_message.is_current_ubuntu)
325+ self.assertFalse(suggestion.is_current_ubuntu)
326+ # Messages are always accepted on the other side, too.
327+ self.assertFalse(imported_message.is_current_upstream)
328+ self.assertTrue(suggestion.is_current_upstream)
329+
330+ def test_accept_current_message(self):
331+ # Accepting a message that's already current does nothing on this
332+ # side but makes sure the other side's flag is set.
333+ pofile = self.factory.makePOFile(side=TranslationSide.UBUNTU)
334+ translation = self.factory.makeCurrentTranslationMessage(
335+ pofile=pofile)
336+ self.assertTrue(translation.is_current_ubuntu)
337+ self.assertFalse(translation.is_current_upstream)
338+
339+ translation.acceptFromUpstreamImportOnPackage(pofile)
340+
341+ self.assertTrue(translation.is_current_ubuntu)
342+ self.assertTrue(translation.is_current_upstream)
343+
344+ def test_accept_current_and_imported_message(self):
345+ # Accepting a message that's already current and was also imported
346+ # does nothing.
347+ pofile = self.factory.makePOFile(side=TranslationSide.UBUNTU)
348+ translation = self.factory.makeCurrentTranslationMessage(
349+ pofile=pofile, current_other=True)
350+ self.assertTrue(translation.is_current_ubuntu)
351+ self.assertTrue(translation.is_current_upstream)
352+
353+ translation.acceptFromUpstreamImportOnPackage(pofile)
354+
355+ self.assertTrue(translation.is_current_ubuntu)
356+ self.assertTrue(translation.is_current_upstream)
357+
358+ def test_accept_detects_conflict(self):
359+ pofile = self.factory.makePOFile(side=TranslationSide.UBUNTU)
360+ current = self.factory.makeCurrentTranslationMessage(pofile=pofile)
361+ potmsgset = current.potmsgset
362+ suggestion = self.factory.makeSuggestion(
363+ pofile=pofile, potmsgset=potmsgset)
364+ old = datetime.now(UTC) - timedelta(days=1)
365+
366+ self.assertRaises(
367+ TranslationConflict,
368+ suggestion.acceptFromUpstreamImportOnPackage,
369+ pofile, lock_timestamp=old)
370+
371+
372 class TestApproveAsDiverged(TestCaseWithFactory):
373 """Tests for `TranslationMessage.approveAsDiverged`."""
374