Merge lp:~adeuring/launchpad/bug-688130 into lp:launchpad

Proposed by Abel Deuring
Status: Merged
Approved by: Abel Deuring
Approved revision: no longer in the source branch.
Merged at revision: 12469
Proposed branch: lp:~adeuring/launchpad/bug-688130
Merge into: lp:launchpad
Diff against target: 329 lines (+166/-14)
5 files modified
lib/lp/testing/factory.py (+18/-0)
lib/lp/translations/model/pofile.py (+12/-5)
lib/lp/translations/tests/test_pofile.py (+128/-0)
lib/lp/translations/tests/test_potmsgset.py (+7/-0)
lib/lp/translations/utilities/rosettastats.py (+1/-9)
To merge this branch: bzr merge lp:~adeuring/launchpad/bug-688130
Reviewer Review Type Date Requested Status
Gavin Panella (community) Approve
Review via email: mp+51107@code.launchpad.net

Commit message

[r=allenap][bug=688130,720199] ensure that factory.makeCurrentTranslation fails if a POTemplate and a POTMsgSet are specified which are not linked by TranslationTemplateItem; do not count current translations which are diverged for the other side as suggestions for 'this' side in another context than the diverged target; tests to ensure that diverged translations in general are not counted as translations or suggestions outside the diverged context; removed a strange and not-working attempt to round floating point numbers to two decimal digits in rosettastats. Consider POTMsgSets which have a plural form but which miss at least one of the plural/singluar variants as untranslated in POTMsgSet._countTranslations()

Description of the change

This branch fixes bug 688130: "Statistics clean-ups and extra tests".

Many translation statistics are calculated in POFile.updateStatistics(), which uses the two methods POFile._countTranslations() and POFile._countNewSuggestions().

These methods did not treat empty translations properly and translations which are only partially translated, i.e. those which required singlar and plural variants.

Jeroen recently fixed the case for empty translations, leaving only the partial translations: If a message is not completely translated, we want to count it as untranslated.

The SQL query in _countTranslations() now uses different expressions for the result column "has_other_msgstrs" and for the WHERE clause "has_msgstrs" to ensure that only TranslationMessage records are counted which have all required plural forms.

_countSuggestions() included translations which are diverged for another target than the POFile/POTemplate for which _countSuggestions() was called; this is fixed too.

As a drive-by fix, I added an "assert" to factory.makeCurrentTranslationMessage(): It allowed to create a translation being diverged for a POTemplate that does not contain the POTMsgSet, i.e., where no TranslationTemplateItem record exists that links the POTMsgSet with the POTemplate.

As another drive-by fix I removed a silly attempt to round a floating point number to two decimal digits from RosettaStats.asPercentage(). There are no tests for this change: The method could not round the result for example of 33/10 anyway (and of most other numbers which, represented as fraction of two integers, have a denominator with any prime factor other than two), so we either already have formatting issues or the callsites already format the values using something like '%3.2f' % (33.0/10) (bug 720199).

test: ./bin/test -vvt test_pofile

no lint

To post a comment you must log in.
Revision history for this message
Gavin Panella (allenap) wrote :

Looks good to me.

[1]

+ tti_for_message_in_template = TranslationTemplateItem.selectOneBy(
+ potmsgset=potmsgset, potemplate=pofile.potemplate)

I think we should try to avoid the SQLObject shim. How about:

    IStore(TranslationTemplateItem).find(
        TranslationTemplateItem.potmsgset == potmsgset,
        TranslationTemplateItem.potemplate == pofile.potemplate).one()

[2]

+ def test_POFile_updateStatistics_diverged_message_this_side(self):
+ # Translations that are diverged for a given target do not
+ # appear in the statistical data for another target.
...
+ def test_POFile_updateStatistics_diverged_message_other_side(self):
+ # Translations that are diverged for a given target do not
+ # appear in the statistical data for another target.

These have the same explanation. Is there a difference?

[3]

I can't pretend to know enough about Translations to be able to do a
really thorough review of this, so if you feel less than confident
about this branch perhaps you should ask a domain expert to go over it
too.

review: Approve

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-22 16:56:05 +0000
3+++ lib/lp/testing/factory.py 2011-02-25 15:04:52 +0000
4@@ -294,6 +294,9 @@
5 from lp.translations.model.translationimportqueue import (
6 TranslationImportQueueEntry,
7 )
8+from lp.translations.model.translationtemplateitem import (
9+ TranslationTemplateItem,
10+ )
11 from lp.translations.utilities.sanitize import (
12 sanitize_translations_from_webui,
13 )
14@@ -2841,6 +2844,8 @@
15 'Cannot specify both language and pofile.')
16 assert None in (side, pofile), (
17 'Cannot specify both side and pofile.')
18+ link_potmsgset_with_potemplate = (
19+ (pofile is None and potemplate is None) or potmsgset is None)
20 if pofile is None:
21 pofile = self.makePOFile(
22 language=language, side=side, potemplate=potemplate)
23@@ -2850,6 +2855,19 @@
24 potemplate = pofile.potemplate
25 if potmsgset is None:
26 potmsgset = self.makePOTMsgSet(potemplate)
27+ if link_potmsgset_with_potemplate:
28+ # If we have a new pofile or a new potmsgset, we must link
29+ # the potmsgset to the pofile's potemplate.
30+ potmsgset.setSequence(
31+ pofile.potemplate, self.getUniqueInteger())
32+ else:
33+ # Otherwise it is the duty of the callsite to ensure
34+ # consistency.
35+ store = IStore(TranslationTemplateItem)
36+ tti_for_message_in_template = store.find(
37+ TranslationTemplateItem.potmsgset == potmsgset,
38+ TranslationTemplateItem.potemplate == pofile.potemplate).any()
39+ assert tti_for_message_in_template is not None
40 if translator is None:
41 translator = self.makePerson()
42 if reviewer is None:
43
44=== modified file 'lib/lp/translations/model/pofile.py'
45--- lib/lp/translations/model/pofile.py 2011-02-22 01:57:30 +0000
46+++ lib/lp/translations/model/pofile.py 2011-02-25 15:04:52 +0000
47@@ -844,15 +844,19 @@
48
49 side_traits = getUtility(ITranslationSideTraitsSet).getForTemplate(
50 self.potemplate)
51+ complete_plural_clause_this_side = ' AND '.join(
52+ self._appendCompletePluralFormsConditions(
53+ [], table_name='Current'))
54+ complete_plural_clause_other_side = ' AND '.join(
55+ self._appendCompletePluralFormsConditions(
56+ [], table_name='Other'))
57 params = {
58 'potemplate': quote(self.potemplate),
59 'language': quote(self.language),
60 'flag': side_traits.flag_name,
61 'other_flag': side_traits.other_side_traits.flag_name,
62- 'has_msgstrs':
63- compose_sql_translationmessage_has_translations("Current"),
64- 'has_other_msgstrs':
65- compose_sql_translationmessage_has_translations("Other"),
66+ 'has_msgstrs': complete_plural_clause_this_side,
67+ 'has_other_msgstrs': complete_plural_clause_other_side,
68 }
69 # The "distinct on" combined with the "order by potemplate nulls
70 # last" makes diverged messages mask their shared equivalents.
71@@ -864,6 +868,7 @@
72 %(has_other_msgstrs)s AS has_other_msgstrs,
73 (Other.id = Current.id) AS same_on_both_sides
74 FROM TranslationTemplateItem AS TTI
75+ JOIN POTMsgSet ON POTMsgSet.id = TTI.potmsgset
76 JOIN TranslationMessage AS Current ON
77 Current.potmsgset = TTI.potmsgset AND
78 Current.language = %(language)s AND
79@@ -950,7 +955,9 @@
80 Suggestion.date_created > COALESCE(
81 Current.date_reviewed,
82 Current.date_created,
83- TIMESTAMP 'epoch')
84+ TIMESTAMP 'epoch') AND
85+ COALESCE(Suggestion.potemplate, %(potemplate)s) =
86+ %(potemplate)s
87 )
88 ORDER BY TTI.potmsgset, Current.potemplate NULLS LAST
89 ) AS messages_with_suggestions
90
91=== modified file 'lib/lp/translations/tests/test_pofile.py'
92--- lib/lp/translations/tests/test_pofile.py 2011-02-22 01:57:30 +0000
93+++ lib/lp/translations/tests/test_pofile.py 2011-02-25 15:04:52 +0000
94@@ -1848,6 +1848,35 @@
95 self.assertEqual(0, self.pofile.newCount())
96 self.assertEqual(0, self.pofile.updatesCount())
97
98+ def test_partial_translations_count_as_untranslated(self):
99+ # A translation requiring plural forms is considered to be
100+ # untranslated if at least one plural translation required
101+ # for the given language is missing.
102+ plural_potmsgset = self.factory.makePOTMsgSet(
103+ self.potemplate, singular='singular-en', plural='plural-en')
104+ self.factory.makeCurrentTranslationMessage(
105+ pofile=self.pofile, potmsgset=plural_potmsgset,
106+ translations=['sr-singular', 'sr-plural-1'])
107+ self.pofile.updateStatistics()
108+ self.assertEqual(0, self.pofile.translatedCount())
109+ self.assertEqual(2, self.pofile.untranslatedCount())
110+ self.assertEqual(0, self.pofile.newCount())
111+ self.assertEqual(0, self.pofile.updatesCount())
112+
113+ def test_complete_translations_count_as_translated(self):
114+ # A translation requiring plural forms is considered to be
115+ # translated if all variants are translated.
116+ plural_potmsgset = self.factory.makePOTMsgSet(
117+ self.potemplate, singular='singular-en', plural='plural-en')
118+ self.factory.makeCurrentTranslationMessage(
119+ pofile=self.pofile, potmsgset=plural_potmsgset,
120+ translations=['sr-singular', 'sr-plural-1', 'sr-plural-2'])
121+ self.pofile.updateStatistics()
122+ self.assertEqual(1, self.pofile.translatedCount())
123+ self.assertEqual(1, self.pofile.untranslatedCount())
124+ self.assertEqual(1, self.pofile.newCount())
125+ self.assertEqual(0, self.pofile.updatesCount())
126+
127 def test_empty_messages_on_this_side_count_as_untranslated(self):
128 # A POTMsgSet whose current TranslationMessage on this side is
129 # empty is counted only as untranslated, regardless of any
130@@ -1863,6 +1892,25 @@
131 self.assertEqual(0, self.pofile.newCount())
132 self.assertEqual(0, self.pofile.updatesCount())
133
134+ def test_partial_messages_on_this_side_count_as_untranslated(self):
135+ # A POTMsgSet whose current TranslationMessage on this side is
136+ # partially translated is considered to be untranslated, regardless
137+ # of any translations it may have on the other side.
138+ plural_potmsgset = self.factory.makePOTMsgSet(
139+ self.potemplate, singular='singular-en', plural='plural-en')
140+ self.factory.makeCurrentTranslationMessage(
141+ pofile=self.pofile, potmsgset=plural_potmsgset,
142+ translations=['sr-singular', 'sr-plural-1'])
143+ other_message = self.factory.makeSuggestion(
144+ pofile=self.pofile, potmsgset=plural_potmsgset,
145+ translations=['sr-ubuntu', 'sr-ubuntu-2', 'sr-ubuntu-3'])
146+ other_message.is_current_ubuntu = True
147+ self.pofile.updateStatistics()
148+ self.assertEqual(0, self.pofile.translatedCount())
149+ self.assertEqual(2, self.pofile.untranslatedCount())
150+ self.assertEqual(0, self.pofile.newCount())
151+ self.assertEqual(0, self.pofile.updatesCount())
152+
153 def test_empty_messages_on_other_side_count_as_untranslated(self):
154 # A POTMsgSet that's translated on this side but has an empty
155 # translation on the other side counts as translated on this
156@@ -1882,6 +1930,29 @@
157 self.assertEqual(0, self.pofile.updatesCount())
158 self.assertEqual(0, self.pofile.currentCount())
159
160+ def test_partial_messages_on_other_side_count_as_untranslated(self):
161+ # A POTMsgSet that's translated on this side and has a different
162+ # partial translation on the other side counts as translated on
163+ # this side, but not equal between both sides (currentCount) or
164+ # translated differently between the two sides (updatesCount).
165+ # Instead, it's counted as translated on this side but not on
166+ # the other (newCount).
167+ plural_potmsgset = self.factory.makePOTMsgSet(
168+ self.potemplate, singular='singular-en', plural='plural-en')
169+ self.factory.makeCurrentTranslationMessage(
170+ pofile=self.pofile, potmsgset=plural_potmsgset,
171+ translations=['sr-singular', 'sr-plural1', 'sr-plural2'])
172+ other_message = self.factory.makeSuggestion(
173+ pofile=self.pofile, potmsgset=plural_potmsgset,
174+ translations=['sr-ubuntu'])
175+ other_message.is_current_ubuntu = True
176+ self.pofile.updateStatistics()
177+ self.assertEqual(1, self.pofile.translatedCount())
178+ self.assertEqual(1, self.pofile.untranslatedCount())
179+ self.assertEqual(1, self.pofile.newCount())
180+ self.assertEqual(0, self.pofile.updatesCount())
181+ self.assertEqual(0, self.pofile.currentCount())
182+
183 def test_tracking_empty_messages_count_as_untranslated(self):
184 # An empty TranslationMessage that's current on both sides
185 # counts as untranslated.
186@@ -1895,6 +1966,59 @@
187 self.assertEqual(0, self.pofile.updatesCount())
188 self.assertEqual(0, self.pofile.currentCount())
189
190+ def test_tracking_partial_translations_count_as_untranslated(self):
191+ # A partial Translations that's current on both sides
192+ # counts as untranslated.
193+ plural_potmsgset = self.factory.makePOTMsgSet(
194+ self.potemplate, singular='singular-en', plural='plural-en')
195+ self.factory.makeCurrentTranslationMessage(
196+ pofile=self.pofile, potmsgset=plural_potmsgset,
197+ translations=['sr-singular', 'sr-plural1'], current_other=True)
198+ self.pofile.updateStatistics()
199+ self.assertEqual(0, self.pofile.translatedCount())
200+ self.assertEqual(2, self.pofile.untranslatedCount())
201+ self.assertEqual(0, self.pofile.newCount())
202+ self.assertEqual(0, self.pofile.updatesCount())
203+ self.assertEqual(0, self.pofile.currentCount())
204+
205+ def makeDivergedTranslationForOtherTarget(self, for_sourcepackage):
206+ """Create a translation message that is diverged for another target.
207+ """
208+ if for_sourcepackage:
209+ ubuntu = getUtility(ILaunchpadCelebrities).ubuntu
210+ distroseries = self.factory.makeDistroSeries(distribution=ubuntu)
211+ sourcepackage = self.factory.makeSourcePackage(
212+ distroseries=distroseries)
213+ sourcepackagename = sourcepackage.sourcepackagename
214+ else:
215+ distroseries = None
216+ sourcepackagename = None
217+ other_potemplate = self.factory.makePOTemplate(
218+ distroseries=distroseries, sourcepackagename=sourcepackagename)
219+ other_pofile = self.factory.makePOFile(
220+ language_code=self.pofile.language.code,
221+ potemplate=other_potemplate)
222+ self.potmsgset.setSequence(
223+ other_potemplate, self.factory.getUniqueInteger())
224+ self.factory.makeCurrentTranslationMessage(
225+ pofile=other_pofile, potmsgset=self.potmsgset, diverged=True)
226+
227+ def test_POFile_updateStatistics_diverged_message_this_side(self):
228+ # Translations that are diverged for a given target on this side
229+ # do not appear in the statistical data for another target.
230+ self.makeDivergedTranslationForOtherTarget(for_sourcepackage=False)
231+ self.pofile.updateStatistics()
232+ self.assertEqual(self.pofile.rosettaCount(), 0)
233+ self.assertEqual(self.pofile.unreviewedCount(), 0)
234+
235+ def test_POFile_updateStatistics_diverged_message_other_side(self):
236+ # Translations that are diverged for a given target on the other side
237+ # do not appear in the statistical data for another target.
238+ self.makeDivergedTranslationForOtherTarget(for_sourcepackage=True)
239+ self.pofile.updateStatistics()
240+ self.assertEqual(self.pofile.rosettaCount(), 0)
241+ self.assertEqual(self.pofile.unreviewedCount(), 0)
242+
243
244 class TestPOFile(TestCaseWithFactory):
245 """Test PO file methods."""
246@@ -2222,6 +2346,8 @@
247 def test_getTranslationMessages_other_pofile(self):
248 # A message from another POFiles is not included.
249 other_pofile = self.factory.makePOFile('de')
250+ self.potmsgset.setSequence(
251+ other_pofile.potemplate, self.factory.getUniqueInteger())
252 self.factory.makeCurrentTranslationMessage(
253 potmsgset=self.potmsgset, pofile=other_pofile)
254
255@@ -2253,6 +2379,8 @@
256 # A message matching given condition but located in another POFile
257 # is not included.
258 other_pofile = self.factory.makePOFile('de')
259+ self.potmsgset.setSequence(
260+ other_pofile.potemplate, self.factory.getUniqueInteger())
261 self.factory.makeCurrentTranslationMessage(
262 potmsgset=self.potmsgset, pofile=other_pofile,
263 diverged=True)
264
265=== modified file 'lib/lp/translations/tests/test_potmsgset.py'
266--- lib/lp/translations/tests/test_potmsgset.py 2011-02-22 21:44:23 +0000
267+++ lib/lp/translations/tests/test_potmsgset.py 2011-02-25 15:04:52 +0000
268@@ -920,6 +920,8 @@
269
270 message_this = self.factory.makeCurrentTranslationMessage(
271 pofile=self.pofile, potmsgset=self.potmsgset)
272+ self.potmsgset.setSequence(
273+ other_potemplate, self.factory.getUniqueInteger())
274 message_other = self.factory.makeCurrentTranslationMessage(
275 pofile=other_pofile, potmsgset=self.potmsgset)
276 traits = getUtility(ITranslationSideTraitsSet).getTraits(
277@@ -1323,6 +1325,7 @@
278 productseries=series2, name=pofile.potemplate.name)
279 pofile2 = template2.getPOFileByLang(pofile.language.code)
280 translation = {0: self.factory.getUniqueString()}
281+ potmsgset.setSequence(template2, self.factory.getUniqueInteger())
282 diverged_message = self.factory.makeCurrentTranslationMessage(
283 pofile=pofile2, potmsgset=potmsgset, translator=owner,
284 translations=translation, diverged=True)
285@@ -1771,6 +1774,8 @@
286 diverged = self.factory.makeDivergedTranslationMessage(
287 pofile=pofile, potmsgset=potmsgset,
288 translations=translations)
289+ potmsgset.setSequence(
290+ other_pofile.potemplate, self.factory.getUniqueInteger())
291 shared = self.factory.makeCurrentTranslationMessage(
292 pofile=other_pofile, potmsgset=potmsgset,
293 translations=translations)
294@@ -1839,6 +1844,8 @@
295 other_pofile = self.factory.makePOFile(pofile.language.code)
296 potmsgset.setSequence(pofile.potemplate, 1)
297
298+ potmsgset.setSequence(
299+ other_pofile.potemplate, self.factory.getUniqueInteger())
300 self.factory.makeCurrentTranslationMessage(
301 pofile=other_pofile, potmsgset=potmsgset,
302 translations=translations, diverged=True)
303
304=== modified file 'lib/lp/translations/utilities/rosettastats.py'
305--- lib/lp/translations/utilities/rosettastats.py 2010-02-08 23:41:43 +0000
306+++ lib/lp/translations/utilities/rosettastats.py 2011-02-25 15:04:52 +0000
307@@ -80,16 +80,9 @@
308 if self.messageCount() > 0:
309 percent = float(value) / self.messageCount()
310 percent *= 100
311- percent = round(percent, 2)
312 else:
313 percent = 0
314- # We use float(str()) to prevent problems with some floating point
315- # representations that could give us:
316- # >>> x = 3.141592
317- # >>> round(x, 2)
318- # 3.1400000000000001
319- # >>>
320- return float(str(percent))
321+ return percent
322
323 def translatedPercentage(self, language=None):
324 """See IRosettaStats."""
325@@ -114,4 +107,3 @@
326 def rosettaPercentage(self, language=None):
327 """See IRosettaStats."""
328 return self.asPercentage(self.rosettaCount(language))
329-