Merge lp:~danilo/launchpad/translatedlanguage into lp:launchpad

Proposed by Данило Шеган
Status: Merged
Merged at revision: 11253
Proposed branch: lp:~danilo/launchpad/translatedlanguage
Merge into: lp:launchpad
Diff against target: 1251 lines (+803/-158)
15 files modified
lib/lp/registry/model/productseries.py (+15/-19)
lib/lp/testing/factory.py (+2/-2)
lib/lp/translations/browser/configure.zcml (+1/-1)
lib/lp/translations/browser/productseries.py (+2/-0)
lib/lp/translations/browser/serieslanguage.py (+11/-8)
lib/lp/translations/configure.zcml (+17/-0)
lib/lp/translations/interfaces/potemplate.py (+14/-0)
lib/lp/translations/interfaces/productserieslanguage.py (+4/-29)
lib/lp/translations/interfaces/translatedlanguage.py (+79/-0)
lib/lp/translations/model/potemplate.py (+10/-3)
lib/lp/translations/model/productserieslanguage.py (+20/-83)
lib/lp/translations/model/translatedlanguage.py (+132/-0)
lib/lp/translations/tests/test_productserieslanguage.py (+15/-13)
lib/lp/translations/tests/test_translatedlanguage.py (+462/-0)
lib/lp/translations/tests/test_translationtemplatescollection.py (+19/-0)
To merge this branch: bzr merge lp:~danilo/launchpad/translatedlanguage
Reviewer Review Type Date Requested Status
Māris Fogels (community) code Approve
Review via email: mp+30788@code.launchpad.net

Commit message

Provide ITranslatedLanguage interface along with a TranslatedLanguageMixin and use that for ProductSeriesLanguage implementation.

Description of the change

= ITranslatedLanguage =

This provides a generic ITranslatedLanguage interface for objects which are a translation of something (i.e. a productseries, distroseries, sourcepackage, template) into a single language, along with a mixin that implements this interface in a generic way.

Mixin is to replace most of the model code on DistroSeriesLanguage and ProductSeriesLanguage, and to be the basis of cleaning up SourcePackageTranslations. It relies on the "parent" object implementing IHasTranslationTemplates with its getCurrentTemplatesCollection() method.

The next steps would be to switch ProductSeriesLanguage, DistroSeriesLanguage and SourcePackageTranslations to make the most of the mixin, but that would result in a huge branch (as if this one isn't big already). So, we only migrate ProductSeriesLanguage in this one (other than a few display-orienteed attributes, the only bits that remain in it are IRosettaStats methods which we want to get rid of as well).

As a preparation for getting rid of IRosettaStats, I introduce a temporary statistics object implementation (a dict) which we want to switch everything to (a better one is in progress in one of Adi's branches).

The most interesting bit of the code is inside the mixin: POFilesByPOTemplates is an iterator-like object which allows slicing over a full set of POTemplates regardless of the presense of POFiles (when they are missing, we return DummyPOFile objects). This ensures we do a constant number of queries for every request.

Unfortunately, for listifying TranslatedLanguageMixin.pofiles when __len__ is defined on POFilesByPOTemplates (a requirement for BatchNavigator), it's always called even if using an iterator would be enough: this means 2 queries instead of 1. However, slicing always does a single query, as confirmed in the test.

It is (somewhat) indirectly unit-tested inside the TranslationTemplateMixinTest, though that's simply because 'pofiles' attribute implementation in the mixin is basically a set-up of POFilesByPOTemplates.

Full test is otherwise written in a way to make it easy to extend for testing over different types of objects implementing ITranslatedLanguage, even though it only tests ProductSeriesLanguage now.

= Tests =

 bin/test -cvvt test_translatedlanguage -t serieslanguage

= Demo & QA =

A few examples:

 https://translations.launchpad.dev/evolution/trunk/+lang/es
 https://translations.launchpad.dev/evolution/trunk/+lang/es?batch=1
 https://translations.launchpad.dev/evolution/trunk/+lang/sr (no PO files)
 https://translations.launchpad.dev/evolution/trunk/+lang/sr?batch=1

And to confirm we haven't broken DistroSeriesLanguage pages:

 https://translations.launchpad.dev/ubuntu/hoary/+lang/es
 https://translations.launchpad.dev/ubuntu/hoary/+lang/es

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/registry/model/productseries.py
  lib/lp/testing/factory.py
  lib/lp/translations/browser/configure.zcml
  lib/lp/translations/browser/serieslanguage.py
  lib/lp/translations/configure.zcml
  lib/lp/translations/interfaces/potemplate.py
  lib/lp/translations/interfaces/productserieslanguage.py
  lib/lp/translations/interfaces/translatedlanguage.py
  lib/lp/translations/model/potemplate.py
  lib/lp/translations/model/productserieslanguage.py
  lib/lp/translations/model/translatedlanguage.py
  lib/lp/translations/tests/test_productserieslanguage.py
  lib/lp/translations/tests/test_translatedlanguage.py
  lib/lp/translations/tests/test_translationtemplatescollection.py

./lib/lp/translations/interfaces/potemplate.py
     736: E301 expected 1 blank line, found 2
     750: E301 expected 1 blank line, found 2
     784: E302 expected 2 blank lines, found 1
    1312: E202 whitespace before ']'
    1400: E202 whitespace before ']'
    1407: E202 whitespace before ']'
    1510: E202 whitespace before ']'

(E301 happens due to comments in interface definition, E202 because of multi-line list definitions; I am not changing these for now, though I did fix a bunch of lint issues; also, these are across two different files: interfaces/potemplate.py and model/potemplate.py, but linter is very buggy)

To post a comment you must log in.
Revision history for this message
Māris Fogels (mars) wrote :

Hi Danilo,

The code looks OK for this branch. I like the object redesign so far. The branch is actually too large for me to review well, but I tried anyway. Here is what I saw:

 • I'm scared by code that passes around large argument lists like ProductSeriesLanguage.setCounts(). Since PSL already recieves a reference to the pofile in it's constructor, is it possible to have PSL pull the counts from the pofile itself internally? Other options are to create a new method PSL.setCountsFromPofile(), or create a new object like PSLCounts(), or just pass in the calculated value: PSL.setCounts(translated).

 • interfaces/translatedlanguage.py should have a copyright of 2010

 • The test in test_TwoTemplatesWithTranslations() could be split up easily in a follow-up branch. That one test method has at least two, if not three, complete tests contained within it.

 • I found the test names in TestTranslatedLanguageMixin a bit vague. For example, "test_parent" looks something like "test_psl_sets_translation_parent", and I can't tell from the tests alone if the test_recalculateCounts_* methods verify the algorithm correctly or not (maybe the Counts tests will be clearer when they are rewritten to test the new Stats object instead of the Mixin).

 • It would be nice to see tests for POFilesByPOTemplates in a follow-up branch. That way you don't have to test it indirectly with expensive and verbose translation setup code in the Mixin tests, the Mixin tests will shrink, and you may be able to skip the database entirely with some fake objects.

Otherwise, I think this looks good. With consideration of the points mentioned, r=mars.

Maris

review: Approve (code)
Revision history for this message
Данило Шеган (danilo) wrote :
Download full text (4.5 KiB)

Hi Maris,

Thanks a lot for reviewing this over-sized branch!

У уто, 27. 07 2010. у 21:34 +0000, Māris Fogels пише:

> The code looks OK for this branch. I like the object redesign so far. The branch is actually too large for me to review well, but I tried anyway. Here is what I saw:
>
> • I'm scared by code that passes around large argument lists like
> ProductSeriesLanguage.setCounts(). Since PSL already recieves a
> reference to the pofile in it's constructor, is it possible to have
> PSL pull the counts from the pofile itself internally? Other options
> are to create a new method PSL.setCountsFromPofile(), or create a new
> object like PSLCounts(), or just pass in the calculated value:
> PSL.setCounts(translated).

Not really: PSL will sometimes have only a single PO file (well,
interestingly, *most* of the time), but a generic case allows it to have
more than one. And for all the stats that we show, we need all these
numbers.

The reason for it's existence is that you sometimes don't want to do a
single query for each of the languages you are fetching (eg.
https://translations.edge.launchpad.net/openobject-addons/trunk gets
aggregated stats from a bunch of stats for ~100 pofiles per language
with one very fast query), so we basically allow "outer" objects to
initialize PSL. When we switch to a stats object itself, we'd be
setting each of them directly, so we won't be passing large arguments
list like this.

> • interfaces/translatedlanguage.py should have a copyright of 2010

Fixed.

> • The test in test_TwoTemplatesWithTranslations() could be split up
> easily in a follow-up branch. That one test method has at least two,
> if not three, complete tests contained within it.

Actually, it also does a lot of assertions that are not needed for that
particular test. Like asserting behaviour that was already tested in
other tests. I've removed those.

I've split it into three tests first, and then actually got rid of the
two that are already tested in test_translatedlanguage
(test_pofiles_two_templates and test_pofiles_two_templates_one_dummy).
The only one left is the one testing the single-POFile optimization
case.

So, thanks to you pointing this out, I've cleaned these tests so we
don't get them repeated between interfaces. It is a separate branch
though (lp:~danilo/launchpad/psl-tests-cleanup).

> • I found the test names in TestTranslatedLanguageMixin a bit vague.
> For example, "test_parent" looks something like
> "test_psl_sets_translation_parent"

test_parent just tests that parent is correctly initialized for a new
object. The naming is directly linked to the attribute being tested. I
name unit tests like that (test_"attribute" or test_"method name", and
then add additional descriptors after that when needed).

> , and I can't tell from the tests
> alone if the test_recalculateCounts_* methods verify the algorithm
> correctly or not (maybe the Counts tests will be clearer when they are
> rewritten to test the new Stats object instead of the Mixin).

These tests are not extensive, and they are going to be hard to figure
out as long as we used the messed-up RosettaStats interface (which
stores values w...

Read more...

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/registry/model/productseries.py'
2--- lib/lp/registry/model/productseries.py 2010-07-15 15:01:18 +0000
3+++ lib/lp/registry/model/productseries.py 2010-07-28 22:36:12 +0000
4@@ -465,12 +465,15 @@
5
6 for language, pofile in ordered_results:
7 psl = ProductSeriesLanguage(self, language, pofile=pofile)
8- psl.setCounts(pofile.potemplate.messageCount(),
9- pofile.currentCount(),
10- pofile.updatesCount(),
11- pofile.rosettaCount(),
12- pofile.unreviewedCount(),
13- pofile.date_changed)
14+ total = pofile.potemplate.messageCount()
15+ imported = pofile.currentCount()
16+ changed = pofile.updatesCount()
17+ rosetta = pofile.rosettaCount()
18+ unreviewed = pofile.unreviewedCount()
19+ translated = imported + rosetta
20+ new = rosetta - changed
21+ psl.setCounts(total, translated, new, changed, unreviewed)
22+ psl.last_changed_date = pofile.date_changed
23 results.append(psl)
24 else:
25 # If there is more than one template, do a single
26@@ -498,22 +501,15 @@
27 POTemplate.iscurrent==True,
28 Language.id!=english.id).group_by(Language)
29
30- # XXX: Ursinha 2009-11-02: The Max(POFile.date_changed) result
31- # here is a naive datetime. My guess is that it happens
32- # because UTC awareness is attibuted to the field in the POFile
33- # model class, and in this case the Max function deals directly
34- # with the value returned from the database without
35- # instantiating it.
36- # This seems to be irrelevant to what we're trying to achieve
37- # here, but making a note either way.
38-
39 ordered_results = query.order_by(['Language.englishname'])
40
41- for (language, imported, changed, new, unreviewed,
42- last_changed) in ordered_results:
43+ for (language, imported, changed, rosetta, unreviewed,
44+ last_changed) in ordered_results:
45 psl = ProductSeriesLanguage(self, language)
46- psl.setCounts(
47- total, imported, changed, new, unreviewed, last_changed)
48+ translated = imported + rosetta
49+ new = rosetta - changed
50+ psl.setCounts(total, translated, new, changed, unreviewed)
51+ psl.last_changed_date = last_changed
52 results.append(psl)
53
54 return results
55
56=== modified file 'lib/lp/testing/factory.py'
57--- lib/lp/testing/factory.py 2010-07-28 19:49:46 +0000
58+++ lib/lp/testing/factory.py 2010-07-28 22:36:12 +0000
59@@ -160,7 +160,6 @@
60 ANONYMOUS,
61 login,
62 login_as,
63- logout,
64 run_with_login,
65 temp_dir,
66 time_counter,
67@@ -846,7 +845,7 @@
68 url = self.getUniqueURL()
69 else:
70 raise UnknownBranchTypeError(
71- 'Unrecognized branch type: %r' % (branch_type,))
72+ 'Unrecognized branch type: %r' % (branch_type, ))
73
74 namespace = get_branch_namespace(
75 owner, product=product, distroseries=distroseries,
76@@ -1633,6 +1632,7 @@
77 return series
78
79 def makeLanguage(self, language_code=None, name=None):
80+ """Makes a language given the language_code and name."""
81 if language_code is None:
82 language_code = self.getUniqueString('lang')
83 if name is None:
84
85=== modified file 'lib/lp/translations/browser/configure.zcml'
86--- lib/lp/translations/browser/configure.zcml 2010-07-16 16:58:55 +0000
87+++ lib/lp/translations/browser/configure.zcml 2010-07-28 22:36:12 +0000
88@@ -269,7 +269,7 @@
89 <browser:url
90 for="lp.translations.interfaces.productserieslanguage.IProductSeriesLanguage"
91 path_expression="string:+lang/${language/code}"
92- attribute_to_parent="productseries"
93+ attribute_to_parent="parent"
94 rootsite="translations"/>
95 <browser:navigation
96 module="lp.translations.browser.serieslanguage"
97
98=== modified file 'lib/lp/translations/browser/productseries.py'
99--- lib/lp/translations/browser/productseries.py 2010-07-22 14:59:48 +0000
100+++ lib/lp/translations/browser/productseries.py 2010-07-28 22:36:12 +0000
101@@ -370,10 +370,12 @@
102 productserieslang = (
103 productserieslangset.getProductSeriesLanguage(
104 self.context, lang, pofile=pofile))
105+ productserieslang.recalculateCounts()
106 else:
107 productserieslang = (
108 productserieslangset.getProductSeriesLanguage(
109 self.context, lang))
110+ productserieslang.recalculateCounts()
111 productserieslangs.append(
112 productserieslang)
113
114
115=== modified file 'lib/lp/translations/browser/serieslanguage.py'
116--- lib/lp/translations/browser/serieslanguage.py 2010-03-04 07:31:38 +0000
117+++ lib/lp/translations/browser/serieslanguage.py 2010-07-28 22:36:12 +0000
118@@ -29,7 +29,7 @@
119
120
121 class BaseSeriesLanguageView(LaunchpadView):
122- """View base class to render translation status for an
123+ """View base class to render translation status for an
124 `IDistroSeries` and `IProductSeries`
125
126 This class should not be directly instantiated.
127@@ -46,12 +46,15 @@
128 self.translationgroup = translationgroup
129 self.form = self.request.form
130
131- self.batchnav = BatchNavigator(
132- self.series.getCurrentTranslationTemplates(),
133- self.request)
134-
135- self.pofiles = self.context.getPOFilesFor(
136- self.batchnav.currentBatch())
137+ if IDistroSeriesLanguage.providedBy(self.context):
138+ self.batchnav = BatchNavigator(
139+ self.series.getCurrentTranslationTemplates(),
140+ self.request)
141+ self.pofiles = self.context.getPOFilesFor(
142+ self.batchnav.currentBatch())
143+ else:
144+ self.batchnav = BatchNavigator(self.context.pofiles, self.request)
145+ self.pofiles = self.batchnav.currentBatch()
146
147 @property
148 def translation_group(self):
149@@ -77,7 +80,7 @@
150 @property
151 def access_level_description(self):
152 """Must not be called when there's no translation group."""
153-
154+
155 if is_read_only():
156 return (
157 "No work can be done on these translations while Launchpad "
158
159=== modified file 'lib/lp/translations/configure.zcml'
160--- lib/lp/translations/configure.zcml 2010-07-22 02:41:43 +0000
161+++ lib/lp/translations/configure.zcml 2010-07-28 22:36:12 +0000
162@@ -399,6 +399,16 @@
163 interface="lp.translations.interfaces.productserieslanguage.IProductSeriesLanguageSet"/>
164 </securedutility>
165
166+ <!-- TranslatedLanguage -->
167+ <facet
168+ facet="translations">
169+ <class
170+ class="lp.translations.model.translatedlanguage.POFilesByPOTemplates">
171+ <allow
172+ interface="lp.translations.interfaces.translatedlanguage.IPOFilesByPOTemplates"/>
173+ </class>
174+ </facet>
175+
176 <!-- POTemplate -->
177 <facet
178 facet="translations">
179@@ -430,6 +440,13 @@
180 provides="lp.translations.interfaces.translationcommonformat.ITranslationFileData"
181 factory="lp.translations.model.potemplate.POTemplateToTranslationFileDataAdapter"/>
182
183+ <!-- TranslationTemplatesCollection -->
184+ <class
185+ class="lp.translations.model.potemplate.TranslationTemplatesCollection">
186+ <allow
187+ interface="lp.translations.interfaces.potemplate.ITranslationTemplatesCollection"/>
188+ </class>
189+
190 <!-- POTemplateSet -->
191
192 <securedutility
193
194=== modified file 'lib/lp/translations/interfaces/potemplate.py'
195--- lib/lp/translations/interfaces/potemplate.py 2010-07-22 14:59:48 +0000
196+++ lib/lp/translations/interfaces/potemplate.py 2010-07-28 22:36:12 +0000
197@@ -781,5 +781,19 @@
198 exist for it.
199 """
200
201+class ITranslationTemplatesCollection(Interface):
202+ """A `Collection` of `POTemplate`s."""
203+
204+ def joinOuterPOFile(language=None):
205+ """Outer-join `POFile` into the collection.
206+
207+ :return: A `TranslationTemplatesCollection` with an added outer
208+ join to `POFile`.
209+ """
210+
211+ def select(*args):
212+ """Return a ResultSet for this collection with values set to args."""
213+
214+
215 # Monkey patch for circular import avoidance done in
216 # _schema_circular_imports.py
217
218=== modified file 'lib/lp/translations/interfaces/productserieslanguage.py'
219--- lib/lp/translations/interfaces/productserieslanguage.py 2010-07-19 15:31:57 +0000
220+++ lib/lp/translations/interfaces/productserieslanguage.py 2010-07-28 22:36:12 +0000
221@@ -5,13 +5,13 @@
222
223 from lazr.restful.fields import Reference
224
225-from zope.interface import Attribute, Interface
226-from zope.schema import (
227- Choice, Datetime, TextLine)
228+from zope.interface import Interface
229+from zope.schema import Choice, TextLine
230
231 from canonical.launchpad import _
232 from lp.translations.interfaces.pofile import IPOFile
233 from lp.translations.interfaces.rosettastats import IRosettaStats
234+from lp.translations.interfaces.translatedlanguage import ITranslatedLanguage
235
236 __metaclass__ = type
237
238@@ -21,13 +21,9 @@
239 ]
240
241
242-class IProductSeriesLanguage(IRosettaStats):
243+class IProductSeriesLanguage(IRosettaStats, ITranslatedLanguage):
244 """Per-language statistics for a product series."""
245
246- language = Choice(
247- title=_('Language to gather statistics for.'),
248- vocabulary='Language', required=True, readonly=True)
249-
250 pofile = Reference(
251 title=_("A POFile if there is only one POTemplate for the series."),
252 schema=IPOFile, required=False, readonly=True)
253@@ -41,27 +37,6 @@
254 title=_("Title for the per-language per-series page."),
255 required=False)
256
257- pofiles = Attribute("The set of pofiles in this distroseries for this "
258- "language. This includes only the real pofiles where translations "
259- "exist.")
260-
261-
262- last_changed_date = Datetime(
263- title=_('When this file was last changed.'))
264-
265- def getPOFilesFor(potemplates):
266- """Return `POFiles` for each of `potemplates`, in the same order.
267-
268- For any `POTemplate` that does not have a translation to the
269- required language, a `DummyPOFile` is provided.
270- """
271-
272- def setCounts(total, imported, changed, new, unreviewed, last_changed):
273- """Set aggregated message counts for ProductSeriesLanguage."""
274-
275- def recalculateCounts(total, imported, changed, new, unreviewed):
276- """Recalculate message counts for this ProductSeriesLanguage."""
277-
278
279 class IProductSeriesLanguageSet(Interface):
280 """The set of productserieslanguages."""
281
282=== added file 'lib/lp/translations/interfaces/translatedlanguage.py'
283--- lib/lp/translations/interfaces/translatedlanguage.py 1970-01-01 00:00:00 +0000
284+++ lib/lp/translations/interfaces/translatedlanguage.py 2010-07-28 22:36:12 +0000
285@@ -0,0 +1,79 @@
286+# Copyright 2010 Canonical Ltd. This software is licensed under the
287+# GNU Affero General Public License version 3 (see the file LICENSE).
288+
289+# pylint: disable-msg=E0211,E0213
290+
291+from zope.interface import Attribute, Interface
292+from zope.interface.common.sequence import IFiniteSequence
293+from zope.schema import Datetime, Object
294+
295+from canonical.launchpad import _
296+from lp.services.worlddata.interfaces.language import ILanguage
297+from lp.translations.interfaces.potemplate import IHasTranslationTemplates
298+from lp.registry.interfaces.person import IPerson
299+
300+__metaclass__ = type
301+
302+__all__ = [
303+ 'IPOFilesByPOTemplates',
304+ 'ITranslatedLanguage',
305+ ]
306+
307+
308+class ITranslatedLanguage(Interface):
309+ """Interface for providing translations for context by language.
310+
311+ It expects `parent` to provide `IHasTranslationTemplates`.
312+ """
313+
314+ language = Object(
315+ title=_('Language to gather statistics and POFiles for.'),
316+ schema=ILanguage)
317+
318+ parent = Object(
319+ title=_('A parent with translation templates.'),
320+ schema=IHasTranslationTemplates)
321+
322+ pofiles = Attribute(
323+ _('Iterator over all POFiles for this context and language.'))
324+
325+ translation_statistics = Attribute(
326+ _('A dict containing relevant aggregated statistics counts.'))
327+
328+ def setCounts(total, translated, new, changed, unreviewed):
329+ """Set aggregated message counts for ITranslatedLanguage."""
330+
331+ def recalculateCounts():
332+ """Recalculate message counts for this ITranslatedLanguage."""
333+
334+ last_changed_date = Datetime(
335+ title=_('When was this translation last changed.'),
336+ readonly=False, required=True)
337+
338+ last_translator = Object(
339+ title=_('Last person that translated something in this context.'),
340+ schema=IPerson)
341+
342+
343+class IPOFilesByPOTemplates(IFiniteSequence):
344+ """Iterate `IPOFile`s for (`ILanguage`, `ITranslationTemplateCollection`).
345+
346+ This is a wrapper for Storm ResultSet that enables optimized slicing
347+ by doing it lazily on the query, thus allowing DummyPOFile objects
348+ to be returned while still not doing more than one database query.
349+
350+ It subclasses `IFiniteSequence` so it can easily be used with the
351+ BatchNavigator.
352+ """
353+
354+ def __getitem__(selector):
355+ """Get an element or slice of `IPOFile`s for given templates."""
356+
357+ def __getslice__(start, end):
358+ """Deprecated, and implemented through __getitem__."""
359+
360+ def __iter__():
361+ """Iterates over all `IPOFile`s for given templates."""
362+
363+ def __len__():
364+ """Provides count of `IPOTemplate`s in a template collection."""
365
366=== modified file 'lib/lp/translations/model/potemplate.py'
367--- lib/lp/translations/model/potemplate.py 2010-07-23 19:44:16 +0000
368+++ lib/lp/translations/model/potemplate.py 2010-07-28 22:36:12 +0000
369@@ -1560,7 +1560,8 @@
370 @property
371 def has_current_translation_templates(self):
372 """See `IHasTranslationTemplates`."""
373- return bool(self.getCurrentTranslationTemplates(just_ids=True).any())
374+ return bool(
375+ self.getCurrentTranslationTemplates(just_ids=True).any())
376
377 def getCurrentTranslationFiles(self, just_ids=False):
378 """See `IHasTranslationTemplates`."""
379@@ -1655,10 +1656,16 @@
380 """
381 return self.joinInner(POFile, POTemplate.id == POFile.potemplateID)
382
383- def joinOuterPOFile(self):
384+ def joinOuterPOFile(self, language=None):
385 """Outer-join `POFile` into the collection.
386
387 :return: A `TranslationTemplatesCollection` with an added outer
388 join to `POFile`.
389 """
390- return self.joinOuter(POFile, POTemplate.id == POFile.potemplateID)
391+ if language is not None:
392+ return self.joinOuter(
393+ POFile, And(POTemplate.id == POFile.potemplateID,
394+ POFile.languageID == language.id))
395+ else:
396+ return self.joinOuter(
397+ POFile, POTemplate.id == POFile.potemplateID)
398
399=== modified file 'lib/lp/translations/model/productserieslanguage.py'
400--- lib/lp/translations/model/productserieslanguage.py 2010-07-19 15:38:51 +0000
401+++ lib/lp/translations/model/productserieslanguage.py 2010-07-28 22:36:12 +0000
402@@ -12,17 +12,13 @@
403
404 from zope.interface import implements
405
406-from storm.expr import Coalesce, Sum
407-from storm.store import Store
408-
409 from lp.translations.utilities.rosettastats import RosettaStats
410-from lp.translations.model.pofile import POFile
411-from lp.translations.model.potemplate import get_pofiles_for, POTemplate
412+from lp.translations.model.translatedlanguage import TranslatedLanguageMixin
413 from lp.translations.interfaces.productserieslanguage import (
414 IProductSeriesLanguage, IProductSeriesLanguageSet)
415
416
417-class ProductSeriesLanguage(RosettaStats):
418+class ProductSeriesLanguage(RosettaStats, TranslatedLanguageMixin):
419 """See `IProductSeriesLanguage`."""
420 implements(IProductSeriesLanguage)
421
422@@ -30,56 +26,14 @@
423 assert 'en' != language.code, (
424 'English is not a translatable language.')
425 RosettaStats.__init__(self)
426+ TranslatedLanguageMixin.__init__(self)
427 self.productseries = productseries
428+ self.parent = productseries
429 self.language = language
430 self.variant = variant
431 self.pofile = pofile
432 self.id = 0
433- self._last_changed_date = None
434-
435- # Reset all cached counts.
436- self.setCounts()
437-
438- def setCounts(self, total=0, imported=0, changed=0, new=0,
439- unreviewed=0, last_changed=None):
440- """See `IProductSeriesLanguage`."""
441- self._messagecount = total
442- # "currentcount" in RosettaStats conflicts our recent terminology
443- # and is closer to "imported" (except that it doesn't include
444- # "changed") translations.
445- self._currentcount = imported
446- self._updatescount = changed
447- self._rosettacount = new
448- self._unreviewed_count = unreviewed
449- if last_changed is not None:
450- self._last_changed_date = last_changed
451-
452- def _getMessageCount(self):
453- store = Store.of(self.language)
454- query = store.find(Sum(POTemplate.messagecount),
455- POTemplate.productseries==self.productseries,
456- POTemplate.iscurrent==True)
457- total, = query
458- if total is None:
459- total = 0
460- return total
461-
462- def recalculateCounts(self):
463- """See `IProductSeriesLanguage`."""
464- store = Store.of(self.language)
465- query = store.find(
466- (Coalesce(Sum(POFile.currentcount), 0),
467- Coalesce(Sum(POFile.updatescount), 0),
468- Coalesce(Sum(POFile.rosettacount), 0),
469- Coalesce(Sum(POFile.unreviewed_count), 0)),
470- POFile.language==self.language,
471- POFile.variant==None,
472- POFile.potemplate==POTemplate.id,
473- POTemplate.productseries==self.productseries,
474- POTemplate.iscurrent==True)
475- imported, changed, new, unreviewed = query[0]
476- self.setCounts(self._getMessageCount(), imported, changed,
477- new, unreviewed)
478+ self.last_changed_date = None
479
480 @property
481 def title(self):
482@@ -90,46 +44,29 @@
483 self.productseries.displayname)
484
485 def messageCount(self):
486- """See `IProductSeriesLanguage`."""
487- return self._messagecount
488+ """See `IRosettaStats`."""
489+ return self._translation_statistics['total_count']
490
491 def currentCount(self, language=None):
492- """See `IProductSeriesLanguage`."""
493- return self._currentcount
494+ """See `IRosettaStats`."""
495+ translated = self._translation_statistics['translated_count']
496+ current = translated - self.rosettaCount(language)
497+ return current
498
499 def updatesCount(self, language=None):
500- """See `IProductSeriesLanguage`."""
501- return self._updatescount
502+ """See `IRosettaStats`."""
503+ return self._translation_statistics['changed_count']
504
505 def rosettaCount(self, language=None):
506- """See `IProductSeriesLanguage`."""
507- return self._rosettacount
508+ """See `IRosettaStats`."""
509+ new = self._translation_statistics['new_count']
510+ changed = self._translation_statistics['changed_count']
511+ rosetta = new + changed
512+ return rosetta
513
514 def unreviewedCount(self):
515- """See `IProductSeriesLanguage`."""
516- return self._unreviewed_count
517-
518- @property
519- def last_changed_date(self):
520- """See `IProductSeriesLanguage`."""
521- return self._last_changed_date
522-
523- @property
524- def pofiles(self):
525- """See `IProductSeriesLanguage`."""
526- store = Store.of(self.language)
527- result = store.find(
528- POFile,
529- POFile.language==self.language,
530- POFile.variant==self.variant,
531- POFile.potemplate==POTemplate.id,
532- POTemplate.productseries==self.productseries,
533- POTemplate.iscurrent==True)
534- return result.order_by(['-priority'])
535-
536- def getPOFilesFor(self, potemplates):
537- """See `IProductSeriesLanguage`."""
538- return get_pofiles_for(potemplates, self.language, self.variant)
539+ """See `IRosettaStats`."""
540+ return self._translation_statistics['unreviewed_count']
541
542
543 class ProductSeriesLanguageSet:
544
545=== added file 'lib/lp/translations/model/translatedlanguage.py'
546--- lib/lp/translations/model/translatedlanguage.py 1970-01-01 00:00:00 +0000
547+++ lib/lp/translations/model/translatedlanguage.py 2010-07-28 22:36:12 +0000
548@@ -0,0 +1,132 @@
549+# Copyright 2010 Canonical Ltd. This software is licensed under the
550+# GNU Affero General Public License version 3 (see the file LICENSE).
551+
552+__all__ = ['TranslatedLanguageMixin']
553+
554+import pytz
555+
556+from zope.interface import implements
557+
558+from storm.expr import Coalesce, Desc, Max, Sum
559+
560+from lp.translations.interfaces.potemplate import IHasTranslationTemplates
561+from lp.translations.interfaces.translatedlanguage import (
562+ IPOFilesByPOTemplates, ITranslatedLanguage)
563+from lp.translations.model.pofile import POFile
564+from lp.translations.model.potemplate import POTemplate
565+
566+
567+class POFilesByPOTemplates(object):
568+ """See `IPOFilesByPOTemplates`."""
569+ implements(IPOFilesByPOTemplates)
570+
571+ def __init__(self, templates_collection, language):
572+ self.templates_collection = templates_collection
573+ self.language = language
574+
575+ def _getDummyOrPOFile(self, potemplate, pofile):
576+ if pofile is None:
577+ return potemplate.getDummyPOFile(self.language,
578+ check_for_existing=False)
579+ else:
580+ return pofile
581+
582+ def _getPOTemplatesAndPOFilesResultSet(self):
583+ current_templates = self.templates_collection
584+ pofiles = current_templates.joinOuterPOFile(self.language)
585+ results = pofiles.select(POTemplate, POFile).order_by(
586+ Desc(POTemplate.priority), POTemplate.name)
587+ return results
588+
589+ def _getPOFilesForResultSet(self, resultset, selector=None):
590+ pofiles_list = []
591+ if selector is None:
592+ results = resultset
593+ else:
594+ results = resultset[selector]
595+ for potemplate, pofile in results:
596+ pofiles_list.append(self._getDummyOrPOFile(potemplate, pofile))
597+ return pofiles_list
598+
599+ def __getitem__(self, selector):
600+ resultset = self._getPOTemplatesAndPOFilesResultSet()
601+ if isinstance(selector, slice):
602+ return self._getPOFilesForResultSet(resultset, selector)
603+ else:
604+ potemplate, pofile = resultset[selector]
605+ return self._getDummyOrPOFile(potemplate, pofile)
606+
607+ def __iter__(self):
608+ resultset = self._getPOTemplatesAndPOFilesResultSet()
609+ for pofile in self._getPOFilesForResultSet(resultset):
610+ yield pofile
611+
612+ def __len__(self):
613+ return self.templates_collection.select(POTemplate).count()
614+
615+ def __nonzero__(self):
616+ return bool(self.templates_collection.select(POTemplate).any())
617+
618+
619+class TranslatedLanguageMixin(object):
620+ """See `ITranslatedLanguage`."""
621+ implements(ITranslatedLanguage)
622+
623+ language = None
624+ parent = None
625+
626+ def __init__(self):
627+ self.setCounts(total=0, translated=0, new=0, changed=0, unreviewed=0)
628+
629+ @property
630+ def pofiles(self):
631+ """See `ITranslatedLanguage`."""
632+ assert IHasTranslationTemplates.providedBy(self.parent), (
633+ "Parent object should implement `IHasTranslationTemplates`.")
634+ current_templates = self.parent.getCurrentTemplatesCollection()
635+ return POFilesByPOTemplates(current_templates, self.language)
636+
637+ @property
638+ def translation_statistics(self):
639+ """See `ITranslatedLanguage`."""
640+ # This is a temporary translation statistics 'object' to allow
641+ # smoother migration from IRosettaStats to something much nicer.
642+ return self._translation_statistics
643+
644+ def setCounts(self, total, translated, new, changed, unreviewed):
645+ """See `ITranslatedLanguage`."""
646+ untranslated = total - translated
647+ self._translation_statistics = {
648+ 'total_count': total,
649+ 'translated_count': translated,
650+ 'new_count': new,
651+ 'changed_count': changed,
652+ 'unreviewed_count': unreviewed,
653+ 'untranslated_count': untranslated,
654+ }
655+
656+ def recalculateCounts(self):
657+ """See `ITranslatedLanguage`."""
658+ templates = self.parent.getCurrentTemplatesCollection()
659+ pofiles = templates.joinOuterPOFile(self.language)
660+ total_count_results = list(
661+ pofiles.select(Coalesce(Sum(POTemplate.messagecount), 0),
662+ Coalesce(Sum(POFile.currentcount), 0),
663+ Coalesce(Sum(POFile.updatescount), 0),
664+ Coalesce(Sum(POFile.rosettacount), 0),
665+ Coalesce(Sum(POFile.unreviewed_count), 0),
666+ Max(POFile.date_changed)))
667+ total, imported, changed, rosetta, unreviewed, date_changed = (
668+ total_count_results[0])
669+ translated = imported + rosetta
670+ new = rosetta - changed
671+ self.setCounts(total, translated, new, changed, unreviewed)
672+
673+ # We have to add a timezone to the otherwise naive-datetime object
674+ # (because we've gotten it using Max() aggregate function).
675+ if date_changed is not None:
676+ date_changed = date_changed.replace(tzinfo=pytz.UTC)
677+ self.last_changed_date = date_changed
678+
679+ last_changed_date = None
680+ last_translator = None
681
682=== modified file 'lib/lp/translations/tests/test_productserieslanguage.py'
683--- lib/lp/translations/tests/test_productserieslanguage.py 2010-07-21 09:35:41 +0000
684+++ lib/lp/translations/tests/test_productserieslanguage.py 2010-07-28 22:36:12 +0000
685@@ -89,8 +89,11 @@
686 self.assertEquals(sr_psl.language, serbian)
687 self.assertEquals(sr_psl.pofile, None)
688
689- # Only this POFile is returned by the `pofiles` property.
690- self.assertEquals(list(sr_psl.pofiles), [pofile1])
691+ # A POFile is returned where it exists, and a DummyPOFile where
692+ # it doesn't.
693+ self.assertEquals(2, len(sr_psl.pofiles))
694+ self.assertEquals(potemplate2, sr_psl.pofiles[0].potemplate)
695+ self.assertEquals(pofile1, sr_psl.pofiles[1])
696
697 # If we provide a POFile for the other template, `pofiles`
698 # returns both (ordered by decreasing priority).
699@@ -173,8 +176,9 @@
700 self.productseries, self.language)
701 self.assertEquals(psl.messageCount(), 0)
702
703- # So, we need to get it through productseries.productserieslanguages.
704- psl = self.productseries.productserieslanguages[0]
705+ # We explicitely ask for stats to be recalculated.
706+ psl.recalculateCounts()
707+
708 self.assertPSLStatistics(psl,
709 (pofile.messageCount(),
710 pofile.translatedCount(),
711@@ -199,17 +203,14 @@
712 self.setPOFileStatistics(pofile2, 1, 1, 1, 1, pofile2.date_changed)
713
714 psl = self.productseries.productserieslanguages[0]
715-
716- # The psl.last_changed_date here is a naive datetime. So, for sake of
717- # the tests, we should make pofile2 naive when checking if it matches
718- # the last calculated changed date, that should be the same as
719- # pofile2, created last.
720+ # We explicitely ask for stats to be recalculated.
721+ psl.recalculateCounts()
722
723 # Total is a sum of totals in both POTemplates (10+20).
724 # Translated is a sum of imported and rosetta translations,
725 # which adds up as (4+3)+(1+1).
726 self.assertPSLStatistics(psl, (30, 9, 5, 4, 3, 6,
727- pofile2.date_changed.replace(tzinfo=None)))
728+ pofile2.date_changed))
729 self.assertPSLStatistics(psl, (
730 pofile1.messageCount() + pofile2.messageCount(),
731 pofile1.translatedCount() + pofile2.translatedCount(),
732@@ -217,7 +218,7 @@
733 pofile1.rosettaCount() + pofile2.rosettaCount(),
734 pofile1.updatesCount() + pofile2.updatesCount(),
735 pofile1.unreviewedCount() + pofile2.unreviewedCount(),
736- pofile2.date_changed.replace(tzinfo=None)))
737+ pofile2.date_changed))
738
739 def test_recalculateCounts(self):
740 # Test that recalculateCounts works correctly.
741@@ -236,13 +237,14 @@
742
743 psl = self.psl_set.getProductSeriesLanguage(self.productseries,
744 self.language)
745- # recalculateCounts() doesn't recalculate the last changed date.
746+
747 psl.recalculateCounts()
748 # Total is a sum of totals in both POTemplates (10+20).
749 # Translated is a sum of imported and rosetta translations,
750 # which adds up as (1+3)+(1+1).
751+ # recalculateCounts() recalculates even the last changed date.
752 self.assertPSLStatistics(psl, (30, 6, 2, 4, 3, 5,
753- None))
754+ pofile2.date_changed))
755
756 def test_recalculateCounts_no_pofiles(self):
757 # Test that recalculateCounts works correctly even when there
758
759=== added file 'lib/lp/translations/tests/test_translatedlanguage.py'
760--- lib/lp/translations/tests/test_translatedlanguage.py 1970-01-01 00:00:00 +0000
761+++ lib/lp/translations/tests/test_translatedlanguage.py 2010-07-28 22:36:12 +0000
762@@ -0,0 +1,462 @@
763+# Copyright 2010 Canonical Ltd. This software is licensed under the
764+# GNU Affero General Public License version 3 (see the file LICENSE).
765+
766+__metaclass__ = type
767+
768+from zope.component import getUtility
769+from zope.interface.verify import verifyObject
770+from zope.security.proxy import removeSecurityProxy
771+
772+from lp.translations.interfaces.productserieslanguage import (
773+ IProductSeriesLanguageSet)
774+from lp.translations.interfaces.translatedlanguage import ITranslatedLanguage
775+from lp.translations.model.pofile import DummyPOFile
776+from lp.testing import TestCaseWithFactory
777+from canonical.testing import ZopelessDatabaseLayer
778+
779+
780+class TestTranslatedLanguageMixin(TestCaseWithFactory):
781+ """Test TranslatedLanguageMixin."""
782+
783+ layer = ZopelessDatabaseLayer
784+
785+ def setUp(self):
786+ # Create a productseries that uses translations.
787+ TestCaseWithFactory.setUp(self)
788+ self.productseries = self.factory.makeProductSeries()
789+ self.productseries.product.official_rosetta = True
790+ self.parent = self.productseries
791+ self.psl_set = getUtility(IProductSeriesLanguageSet)
792+ self.language = self.factory.makeLanguage('sr@test')
793+
794+ def getTranslatedLanguage(self, language):
795+ return self.psl_set.getProductSeriesLanguage(self.productseries,
796+ language)
797+
798+ def addPOTemplate(self, number_of_potmsgsets=0, priority=0):
799+ potemplate = self.factory.makePOTemplate(
800+ productseries=self.productseries)
801+ for sequence in range(number_of_potmsgsets):
802+ self.factory.makePOTMsgSet(potemplate, sequence=sequence+1)
803+ removeSecurityProxy(potemplate).messagecount = number_of_potmsgsets
804+ potemplate.priority = priority
805+ return potemplate
806+
807+ def test_interface(self):
808+ translated_language = self.getTranslatedLanguage(self.language)
809+ self.assertTrue(verifyObject(ITranslatedLanguage,
810+ translated_language))
811+
812+ def test_language(self):
813+ translated_language = self.getTranslatedLanguage(self.language)
814+ self.assertEqual(self.language,
815+ translated_language.language)
816+
817+ def test_parent(self):
818+ translated_language = self.getTranslatedLanguage(self.language)
819+ self.assertEqual(self.parent,
820+ translated_language.parent)
821+
822+ def test_pofiles_notemplates(self):
823+ translated_language = self.getTranslatedLanguage(self.language)
824+ self.assertEqual([], list(translated_language.pofiles))
825+
826+ def test_pofiles_template_no_pofiles(self):
827+ translated_language = self.getTranslatedLanguage(self.language)
828+ potemplate = self.addPOTemplate()
829+ dummy_pofile = potemplate.getDummyPOFile(self.language)
830+ pofiles = list(translated_language.pofiles)
831+ self.assertEqual(1, len(pofiles))
832+
833+ # When there are no actual PO files, we get a DummyPOFile object
834+ # instead.
835+ dummy_pofile = pofiles[0]
836+ naked_dummy = removeSecurityProxy(dummy_pofile)
837+ self.assertEqual(DummyPOFile, type(naked_dummy))
838+ self.assertEqual(self.language, dummy_pofile.language)
839+ self.assertEqual(potemplate, dummy_pofile.potemplate)
840+
841+ # Two queries get executed when listifying
842+ # TranslatedLanguageMixin.pofiles: a len() does a count, and
843+ # then all POTemplates and POFiles are fetched with the other.
844+ self.assertStatementCount(2, list, translated_language.pofiles)
845+
846+ def test_pofiles_template_with_pofiles(self):
847+ translated_language = self.getTranslatedLanguage(self.language)
848+ potemplate = self.addPOTemplate()
849+ pofile = self.factory.makePOFile(self.language.code, potemplate)
850+ self.assertEqual([pofile], list(translated_language.pofiles))
851+
852+ # Two queries get executed when listifying
853+ # TranslatedLanguageMixin.pofiles: a len() does a count, and
854+ # then all POTemplates and POFiles are fetched with the other.
855+ self.assertStatementCount(2, list, translated_language.pofiles)
856+
857+ def test_pofiles_two_templates(self):
858+ translated_language = self.getTranslatedLanguage(self.language)
859+ # Two templates with different priorities so they get sorted
860+ # appropriately.
861+ potemplate1 = self.addPOTemplate(priority=2)
862+ pofile1 = self.factory.makePOFile(self.language.code, potemplate1)
863+ potemplate2 = self.addPOTemplate(priority=1)
864+ pofile2 = self.factory.makePOFile(self.language.code, potemplate2)
865+ self.assertEqual([pofile1, pofile2],
866+ list(translated_language.pofiles))
867+
868+ # Two queries get executed when listifying
869+ # TranslatedLanguageMixin.pofiles: a len() does a count, and
870+ # then all POTemplates and POFiles are fetched with the other.
871+ self.assertStatementCount(2, list, translated_language.pofiles)
872+
873+ def test_pofiles_two_templates_one_dummy(self):
874+ translated_language = self.getTranslatedLanguage(self.language)
875+ # Two templates with different priorities so they get sorted
876+ # appropriately.
877+ potemplate1 = self.addPOTemplate(priority=2)
878+ pofile1 = self.factory.makePOFile(self.language.code, potemplate1)
879+ potemplate2 = self.addPOTemplate(priority=1)
880+ pofiles = translated_language.pofiles
881+ self.assertEqual(pofile1, pofiles[0])
882+ dummy_pofile = removeSecurityProxy(pofiles[1])
883+ self.assertEqual(DummyPOFile, type(dummy_pofile))
884+
885+ # Two queries get executed when listifying
886+ # TranslatedLanguageMixin.pofiles: a len() does a count, and
887+ # then all POTemplates and POFiles are fetched with the other.
888+ self.assertStatementCount(2, list, translated_language.pofiles)
889+
890+ def test_pofiles_slicing(self):
891+ # Slicing still works, and always does the same constant number
892+ # of queries (1).
893+ translated_language = self.getTranslatedLanguage(self.language)
894+ # Three templates with different priorities so they get sorted
895+ # appropriately.
896+ potemplate1 = self.addPOTemplate(priority=2)
897+ pofile1 = self.factory.makePOFile(self.language.code, potemplate1)
898+ potemplate2 = self.addPOTemplate(priority=1)
899+ pofile2 = self.factory.makePOFile(self.language.code, potemplate2)
900+ potemplate3 = self.addPOTemplate(priority=0)
901+
902+ pofiles = translated_language.pofiles[0:2]
903+ self.assertEqual([pofile1, pofile2], list(pofiles))
904+
905+ # Slicing executes only a single query.
906+ get_slice = lambda of, start, end: list(of[start:end])
907+ self.assertStatementCount(1, get_slice,
908+ translated_language.pofiles, 1, 3)
909+
910+ def test_pofiles_slicing_dummies(self):
911+ # Slicing includes DummyPOFiles.
912+ translated_language = self.getTranslatedLanguage(self.language)
913+ # Three templates with different priorities so they get sorted
914+ # appropriately.
915+ potemplate1 = self.addPOTemplate(priority=2)
916+ pofile1 = self.factory.makePOFile(self.language.code, potemplate1)
917+ potemplate2 = self.addPOTemplate(priority=1)
918+ pofile2 = self.factory.makePOFile(self.language.code, potemplate2)
919+ potemplate3 = self.addPOTemplate(priority=0)
920+
921+ pofiles = translated_language.pofiles[1:3]
922+ self.assertEqual(pofile2, pofiles[0])
923+ dummy_pofile = removeSecurityProxy(pofiles[1])
924+ self.assertEqual(DummyPOFile, type(dummy_pofile))
925+
926+ def test_statistics_empty(self):
927+ translated_language = self.getTranslatedLanguage(self.language)
928+
929+ expected = {
930+ 'total_count': 0,
931+ 'translated_count': 0,
932+ 'new_count': 0,
933+ 'changed_count': 0,
934+ 'unreviewed_count': 0,
935+ 'untranslated_count': 0,
936+ }
937+ self.assertEqual(expected,
938+ translated_language.translation_statistics)
939+
940+ def test_setCounts_statistics(self):
941+ translated_language = self.getTranslatedLanguage(self.language)
942+
943+ total = 5
944+ translated = 4
945+ new = 3
946+ changed = 2
947+ unreviewed = 1
948+ untranslated = total - translated
949+
950+ translated_language.setCounts(
951+ total, translated, new, changed, unreviewed)
952+
953+ expected = {
954+ 'total_count': total,
955+ 'translated_count': translated,
956+ 'new_count': new,
957+ 'changed_count': changed,
958+ 'unreviewed_count': unreviewed,
959+ 'untranslated_count': untranslated,
960+ }
961+ self.assertEqual(expected,
962+ translated_language.translation_statistics)
963+
964+ def test_recalculateCounts_empty(self):
965+ translated_language = self.getTranslatedLanguage(self.language)
966+
967+ translated_language.recalculateCounts()
968+
969+ expected = {
970+ 'total_count': 0,
971+ 'translated_count': 0,
972+ 'new_count': 0,
973+ 'changed_count': 0,
974+ 'unreviewed_count': 0,
975+ 'untranslated_count': 0,
976+ }
977+ self.assertEqual(expected,
978+ translated_language.translation_statistics)
979+
980+ def test_recalculateCounts_total_one_pofile(self):
981+ translated_language = self.getTranslatedLanguage(self.language)
982+ potemplate = self.addPOTemplate(number_of_potmsgsets=5)
983+ pofile = self.factory.makePOFile(self.language.code, potemplate)
984+
985+ translated_language.recalculateCounts()
986+ self.assertEqual(
987+ 5, translated_language.translation_statistics['total_count'])
988+
989+ def test_recalculateCounts_total_two_pofiles(self):
990+ translated_language = self.getTranslatedLanguage(self.language)
991+ potemplate1 = self.addPOTemplate(number_of_potmsgsets=5)
992+ pofile1 = self.factory.makePOFile(self.language.code, potemplate1)
993+ potemplate2 = self.addPOTemplate(number_of_potmsgsets=3)
994+ pofile2 = self.factory.makePOFile(self.language.code, potemplate2)
995+
996+ translated_language.recalculateCounts()
997+ self.assertEqual(
998+ 5+3, translated_language.translation_statistics['total_count'])
999+
1000+ def test_recalculateCounts_translated_one_pofile(self):
1001+ translated_language = self.getTranslatedLanguage(self.language)
1002+ potemplate = self.addPOTemplate(number_of_potmsgsets=5)
1003+ pofile = self.factory.makePOFile(self.language.code, potemplate)
1004+ naked_pofile = removeSecurityProxy(pofile)
1005+ # translated count is current + rosetta
1006+ naked_pofile.currentcount = 3
1007+ naked_pofile.rosettacount = 1
1008+
1009+ translated_language.recalculateCounts()
1010+ self.assertEqual(
1011+ 4, translated_language.translation_statistics['translated_count'])
1012+
1013+ def test_recalculateCounts_translated_two_pofiles(self):
1014+ translated_language = self.getTranslatedLanguage(self.language)
1015+ potemplate1 = self.addPOTemplate(number_of_potmsgsets=5)
1016+ pofile1 = self.factory.makePOFile(self.language.code, potemplate1)
1017+ naked_pofile1 = removeSecurityProxy(pofile1)
1018+ # translated count is current + rosetta
1019+ naked_pofile1.currentcount = 3
1020+ naked_pofile1.rosettacount = 1
1021+
1022+ potemplate2 = self.addPOTemplate(number_of_potmsgsets=3)
1023+ pofile2 = self.factory.makePOFile(self.language.code, potemplate2)
1024+ naked_pofile2 = removeSecurityProxy(pofile2)
1025+ # translated count is current + rosetta
1026+ naked_pofile2.currentcount = 1
1027+ naked_pofile2.rosettacount = 1
1028+
1029+ translated_language.recalculateCounts()
1030+ self.assertEqual(
1031+ 6, translated_language.translation_statistics['translated_count'])
1032+
1033+ def test_recalculateCounts_changed_one_pofile(self):
1034+ translated_language = self.getTranslatedLanguage(self.language)
1035+ potemplate = self.addPOTemplate(number_of_potmsgsets=5)
1036+ pofile = self.factory.makePOFile(self.language.code, potemplate)
1037+ naked_pofile = removeSecurityProxy(pofile)
1038+ # translated count is current + rosetta
1039+ naked_pofile.updatescount = 3
1040+
1041+ translated_language.recalculateCounts()
1042+ self.assertEqual(
1043+ 3, translated_language.translation_statistics['changed_count'])
1044+
1045+ def test_recalculateCounts_changed_two_pofiles(self):
1046+ translated_language = self.getTranslatedLanguage(self.language)
1047+ potemplate1 = self.addPOTemplate(number_of_potmsgsets=5)
1048+ pofile1 = self.factory.makePOFile(self.language.code, potemplate1)
1049+ naked_pofile1 = removeSecurityProxy(pofile1)
1050+ naked_pofile1.updatescount = 3
1051+
1052+ potemplate2 = self.addPOTemplate(number_of_potmsgsets=3)
1053+ pofile2 = self.factory.makePOFile(self.language.code, potemplate2)
1054+ naked_pofile2 = removeSecurityProxy(pofile2)
1055+ naked_pofile2.updatescount = 1
1056+
1057+ translated_language.recalculateCounts()
1058+ self.assertEqual(
1059+ 4, translated_language.translation_statistics['changed_count'])
1060+
1061+ def test_recalculateCounts_new_one_pofile(self):
1062+ translated_language = self.getTranslatedLanguage(self.language)
1063+ potemplate = self.addPOTemplate(number_of_potmsgsets=5)
1064+ pofile = self.factory.makePOFile(self.language.code, potemplate)
1065+ naked_pofile = removeSecurityProxy(pofile)
1066+ # new count is rosetta - changed
1067+ naked_pofile.rosettacount = 3
1068+ naked_pofile.updatescount = 1
1069+
1070+ translated_language.recalculateCounts()
1071+ self.assertEqual(
1072+ 2, translated_language.translation_statistics['new_count'])
1073+
1074+ def test_recalculateCounts_new_two_pofiles(self):
1075+ translated_language = self.getTranslatedLanguage(self.language)
1076+ potemplate1 = self.addPOTemplate(number_of_potmsgsets=5)
1077+ pofile1 = self.factory.makePOFile(self.language.code, potemplate1)
1078+ naked_pofile1 = removeSecurityProxy(pofile1)
1079+ # new count is rosetta - changed
1080+ naked_pofile1.rosettacount = 3
1081+ naked_pofile1.updatescount = 1
1082+
1083+ potemplate2 = self.addPOTemplate(number_of_potmsgsets=3)
1084+ pofile2 = self.factory.makePOFile(self.language.code, potemplate2)
1085+ naked_pofile2 = removeSecurityProxy(pofile2)
1086+ # new count is rosetta - changed
1087+ naked_pofile2.rosettacount = 2
1088+ naked_pofile2.updatescount = 1
1089+
1090+ translated_language.recalculateCounts()
1091+ self.assertEqual(
1092+ 3, translated_language.translation_statistics['new_count'])
1093+
1094+ def test_recalculateCounts_unreviewed_one_pofile(self):
1095+ translated_language = self.getTranslatedLanguage(self.language)
1096+ potemplate = self.addPOTemplate(number_of_potmsgsets=5)
1097+ pofile = self.factory.makePOFile(self.language.code, potemplate)
1098+ naked_pofile = removeSecurityProxy(pofile)
1099+ # translated count is current + rosetta
1100+ naked_pofile.unreviewed_count = 3
1101+
1102+ translated_language.recalculateCounts()
1103+ self.assertEqual(
1104+ 3, translated_language.translation_statistics['unreviewed_count'])
1105+
1106+ def test_recalculateCounts_unreviewed_two_pofiles(self):
1107+ translated_language = self.getTranslatedLanguage(self.language)
1108+ potemplate1 = self.addPOTemplate(number_of_potmsgsets=5)
1109+ pofile1 = self.factory.makePOFile(self.language.code, potemplate1)
1110+ naked_pofile1 = removeSecurityProxy(pofile1)
1111+ naked_pofile1.unreviewed_count = 3
1112+
1113+ potemplate2 = self.addPOTemplate(number_of_potmsgsets=3)
1114+ pofile2 = self.factory.makePOFile(self.language.code, potemplate2)
1115+ naked_pofile2 = removeSecurityProxy(pofile2)
1116+ naked_pofile2.unreviewed_count = 1
1117+
1118+ translated_language.recalculateCounts()
1119+ self.assertEqual(
1120+ 4, translated_language.translation_statistics['unreviewed_count'])
1121+
1122+ def test_recalculateCounts_one_pofile(self):
1123+ translated_language = self.getTranslatedLanguage(self.language)
1124+ potemplate = self.addPOTemplate(number_of_potmsgsets=5)
1125+ pofile = self.factory.makePOFile(self.language.code, potemplate)
1126+ naked_pofile = removeSecurityProxy(pofile)
1127+ # translated count is current + rosetta
1128+ naked_pofile.currentcount = 3
1129+ naked_pofile.rosettacount = 1
1130+ # Changed count is 'updatescount' on POFile.
1131+ # It has to be lower or equal to currentcount.
1132+ naked_pofile.updatescount = 1
1133+ # new is rosettacount-updatescount.
1134+ naked_pofile.newcount = 0
1135+ naked_pofile.unreviewed_count = 3
1136+
1137+ translated_language.recalculateCounts()
1138+
1139+ expected = {
1140+ 'total_count': 5,
1141+ 'translated_count': 4,
1142+ 'new_count': 0,
1143+ 'changed_count': 1,
1144+ 'unreviewed_count': 3,
1145+ 'untranslated_count': 1,
1146+ }
1147+ self.assertEqual(expected,
1148+ translated_language.translation_statistics)
1149+
1150+ def test_recalculateCounts_two_pofiles(self):
1151+ translated_language = self.getTranslatedLanguage(self.language)
1152+
1153+ # Set up one template with a single PO file.
1154+ potemplate1 = self.addPOTemplate(number_of_potmsgsets=5)
1155+ pofile1 = self.factory.makePOFile(self.language.code, potemplate1)
1156+ naked_pofile1 = removeSecurityProxy(pofile1)
1157+ # translated count is current + rosetta
1158+ naked_pofile1.currentcount = 2
1159+ naked_pofile1.rosettacount = 2
1160+ # Changed count is 'updatescount' on POFile.
1161+ # It has to be lower or equal to currentcount.
1162+ # new is rosettacount-updatescount.
1163+ naked_pofile1.updatescount = 1
1164+ naked_pofile1.unreviewed_count = 3
1165+
1166+ # Set up second template with a single PO file.
1167+ potemplate2 = self.addPOTemplate(number_of_potmsgsets=3)
1168+ pofile2 = self.factory.makePOFile(self.language.code, potemplate2)
1169+ naked_pofile2 = removeSecurityProxy(pofile2)
1170+ # translated count is current + rosetta
1171+ naked_pofile2.currentcount = 1
1172+ naked_pofile2.rosettacount = 2
1173+ # Changed count is 'updatescount' on POFile.
1174+ # It has to be lower or equal to currentcount.
1175+ # new is rosettacount-updatescount.
1176+ naked_pofile2.updatescount = 1
1177+ naked_pofile2.unreviewed_count = 1
1178+
1179+ translated_language.recalculateCounts()
1180+
1181+ expected = {
1182+ 'total_count': 8,
1183+ 'translated_count': 7,
1184+ 'new_count': 2,
1185+ 'changed_count': 2,
1186+ 'unreviewed_count': 4,
1187+ 'untranslated_count': 1,
1188+ }
1189+ self.assertEqual(expected,
1190+ translated_language.translation_statistics)
1191+
1192+ def test_recalculateCounts_two_templates_one_translation(self):
1193+ # Make sure recalculateCounts works even if a POFile is missing
1194+ # for one of the templates.
1195+ translated_language = self.getTranslatedLanguage(self.language)
1196+
1197+ # Set up one template with a single PO file.
1198+ potemplate1 = self.addPOTemplate(number_of_potmsgsets=5)
1199+ pofile1 = self.factory.makePOFile(self.language.code, potemplate1)
1200+ naked_pofile1 = removeSecurityProxy(pofile1)
1201+ # translated count is current + rosetta
1202+ naked_pofile1.currentcount = 2
1203+ naked_pofile1.rosettacount = 2
1204+ # Changed count is 'updatescount' on POFile.
1205+ # It has to be lower or equal to currentcount.
1206+ # new is rosettacount-updatescount.
1207+ naked_pofile1.updatescount = 1
1208+ naked_pofile1.unreviewed_count = 3
1209+
1210+ # Set up second template with a single PO file.
1211+ potemplate2 = self.addPOTemplate(number_of_potmsgsets=3)
1212+
1213+ translated_language.recalculateCounts()
1214+
1215+ expected = {
1216+ 'total_count': 8,
1217+ 'translated_count': 4,
1218+ 'new_count': 1,
1219+ 'changed_count': 1,
1220+ 'unreviewed_count': 3,
1221+ 'untranslated_count': 4,
1222+ }
1223+ self.assertEqual(expected,
1224+ translated_language.translation_statistics)
1225
1226=== modified file 'lib/lp/translations/tests/test_translationtemplatescollection.py'
1227--- lib/lp/translations/tests/test_translationtemplatescollection.py 2010-07-17 16:19:38 +0000
1228+++ lib/lp/translations/tests/test_translationtemplatescollection.py 2010-07-28 22:36:12 +0000
1229@@ -205,3 +205,22 @@
1230 ]
1231 self.assertContentEqual(
1232 expected_outcome, joined.select(POTemplate, POFile))
1233+
1234+ def test_joinOuterPOFile_language(self):
1235+ trunk = self.factory.makeProduct().getSeries('trunk')
1236+ translated_template = self.factory.makePOTemplate(productseries=trunk)
1237+ untranslated_template = self.factory.makePOTemplate(
1238+ productseries=trunk)
1239+ nl = translated_template.newPOFile('nl')
1240+ de = translated_template.newPOFile('de')
1241+
1242+ collection = TranslationTemplatesCollection()
1243+ by_series = collection.restrictProductSeries(trunk)
1244+ joined = by_series.joinOuterPOFile(language=nl.language)
1245+
1246+ expected_outcome = [
1247+ (translated_template, nl),
1248+ (untranslated_template, None),
1249+ ]
1250+ self.assertContentEqual(
1251+ expected_outcome, joined.select(POTemplate, POFile))