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
=== modified file 'lib/lp/registry/model/productseries.py'
--- lib/lp/registry/model/productseries.py 2010-07-15 15:01:18 +0000
+++ lib/lp/registry/model/productseries.py 2010-07-28 22:36:12 +0000
@@ -465,12 +465,15 @@
465465
466 for language, pofile in ordered_results:466 for language, pofile in ordered_results:
467 psl = ProductSeriesLanguage(self, language, pofile=pofile)467 psl = ProductSeriesLanguage(self, language, pofile=pofile)
468 psl.setCounts(pofile.potemplate.messageCount(),468 total = pofile.potemplate.messageCount()
469 pofile.currentCount(),469 imported = pofile.currentCount()
470 pofile.updatesCount(),470 changed = pofile.updatesCount()
471 pofile.rosettaCount(),471 rosetta = pofile.rosettaCount()
472 pofile.unreviewedCount(),472 unreviewed = pofile.unreviewedCount()
473 pofile.date_changed)473 translated = imported + rosetta
474 new = rosetta - changed
475 psl.setCounts(total, translated, new, changed, unreviewed)
476 psl.last_changed_date = pofile.date_changed
474 results.append(psl)477 results.append(psl)
475 else:478 else:
476 # If there is more than one template, do a single479 # If there is more than one template, do a single
@@ -498,22 +501,15 @@
498 POTemplate.iscurrent==True,501 POTemplate.iscurrent==True,
499 Language.id!=english.id).group_by(Language)502 Language.id!=english.id).group_by(Language)
500503
501 # XXX: Ursinha 2009-11-02: The Max(POFile.date_changed) result
502 # here is a naive datetime. My guess is that it happens
503 # because UTC awareness is attibuted to the field in the POFile
504 # model class, and in this case the Max function deals directly
505 # with the value returned from the database without
506 # instantiating it.
507 # This seems to be irrelevant to what we're trying to achieve
508 # here, but making a note either way.
509
510 ordered_results = query.order_by(['Language.englishname'])504 ordered_results = query.order_by(['Language.englishname'])
511505
512 for (language, imported, changed, new, unreviewed,506 for (language, imported, changed, rosetta, unreviewed,
513 last_changed) in ordered_results:507 last_changed) in ordered_results:
514 psl = ProductSeriesLanguage(self, language)508 psl = ProductSeriesLanguage(self, language)
515 psl.setCounts(509 translated = imported + rosetta
516 total, imported, changed, new, unreviewed, last_changed)510 new = rosetta - changed
511 psl.setCounts(total, translated, new, changed, unreviewed)
512 psl.last_changed_date = last_changed
517 results.append(psl)513 results.append(psl)
518514
519 return results515 return results
520516
=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py 2010-07-28 19:49:46 +0000
+++ lib/lp/testing/factory.py 2010-07-28 22:36:12 +0000
@@ -160,7 +160,6 @@
160 ANONYMOUS,160 ANONYMOUS,
161 login,161 login,
162 login_as,162 login_as,
163 logout,
164 run_with_login,163 run_with_login,
165 temp_dir,164 temp_dir,
166 time_counter,165 time_counter,
@@ -846,7 +845,7 @@
846 url = self.getUniqueURL()845 url = self.getUniqueURL()
847 else:846 else:
848 raise UnknownBranchTypeError(847 raise UnknownBranchTypeError(
849 'Unrecognized branch type: %r' % (branch_type,))848 'Unrecognized branch type: %r' % (branch_type, ))
850849
851 namespace = get_branch_namespace(850 namespace = get_branch_namespace(
852 owner, product=product, distroseries=distroseries,851 owner, product=product, distroseries=distroseries,
@@ -1633,6 +1632,7 @@
1633 return series1632 return series
16341633
1635 def makeLanguage(self, language_code=None, name=None):1634 def makeLanguage(self, language_code=None, name=None):
1635 """Makes a language given the language_code and name."""
1636 if language_code is None:1636 if language_code is None:
1637 language_code = self.getUniqueString('lang')1637 language_code = self.getUniqueString('lang')
1638 if name is None:1638 if name is None:
16391639
=== modified file 'lib/lp/translations/browser/configure.zcml'
--- lib/lp/translations/browser/configure.zcml 2010-07-16 16:58:55 +0000
+++ lib/lp/translations/browser/configure.zcml 2010-07-28 22:36:12 +0000
@@ -269,7 +269,7 @@
269 <browser:url269 <browser:url
270 for="lp.translations.interfaces.productserieslanguage.IProductSeriesLanguage"270 for="lp.translations.interfaces.productserieslanguage.IProductSeriesLanguage"
271 path_expression="string:+lang/${language/code}"271 path_expression="string:+lang/${language/code}"
272 attribute_to_parent="productseries"272 attribute_to_parent="parent"
273 rootsite="translations"/>273 rootsite="translations"/>
274 <browser:navigation274 <browser:navigation
275 module="lp.translations.browser.serieslanguage"275 module="lp.translations.browser.serieslanguage"
276276
=== modified file 'lib/lp/translations/browser/productseries.py'
--- lib/lp/translations/browser/productseries.py 2010-07-22 14:59:48 +0000
+++ lib/lp/translations/browser/productseries.py 2010-07-28 22:36:12 +0000
@@ -370,10 +370,12 @@
370 productserieslang = (370 productserieslang = (
371 productserieslangset.getProductSeriesLanguage(371 productserieslangset.getProductSeriesLanguage(
372 self.context, lang, pofile=pofile))372 self.context, lang, pofile=pofile))
373 productserieslang.recalculateCounts()
373 else:374 else:
374 productserieslang = (375 productserieslang = (
375 productserieslangset.getProductSeriesLanguage(376 productserieslangset.getProductSeriesLanguage(
376 self.context, lang))377 self.context, lang))
378 productserieslang.recalculateCounts()
377 productserieslangs.append(379 productserieslangs.append(
378 productserieslang)380 productserieslang)
379381
380382
=== modified file 'lib/lp/translations/browser/serieslanguage.py'
--- lib/lp/translations/browser/serieslanguage.py 2010-03-04 07:31:38 +0000
+++ lib/lp/translations/browser/serieslanguage.py 2010-07-28 22:36:12 +0000
@@ -29,7 +29,7 @@
2929
3030
31class BaseSeriesLanguageView(LaunchpadView):31class BaseSeriesLanguageView(LaunchpadView):
32 """View base class to render translation status for an 32 """View base class to render translation status for an
33 `IDistroSeries` and `IProductSeries`33 `IDistroSeries` and `IProductSeries`
3434
35 This class should not be directly instantiated.35 This class should not be directly instantiated.
@@ -46,12 +46,15 @@
46 self.translationgroup = translationgroup46 self.translationgroup = translationgroup
47 self.form = self.request.form47 self.form = self.request.form
4848
49 self.batchnav = BatchNavigator(49 if IDistroSeriesLanguage.providedBy(self.context):
50 self.series.getCurrentTranslationTemplates(),50 self.batchnav = BatchNavigator(
51 self.request)51 self.series.getCurrentTranslationTemplates(),
5252 self.request)
53 self.pofiles = self.context.getPOFilesFor(53 self.pofiles = self.context.getPOFilesFor(
54 self.batchnav.currentBatch())54 self.batchnav.currentBatch())
55 else:
56 self.batchnav = BatchNavigator(self.context.pofiles, self.request)
57 self.pofiles = self.batchnav.currentBatch()
5558
56 @property59 @property
57 def translation_group(self):60 def translation_group(self):
@@ -77,7 +80,7 @@
77 @property80 @property
78 def access_level_description(self):81 def access_level_description(self):
79 """Must not be called when there's no translation group."""82 """Must not be called when there's no translation group."""
80 83
81 if is_read_only():84 if is_read_only():
82 return (85 return (
83 "No work can be done on these translations while Launchpad "86 "No work can be done on these translations while Launchpad "
8487
=== modified file 'lib/lp/translations/configure.zcml'
--- lib/lp/translations/configure.zcml 2010-07-22 02:41:43 +0000
+++ lib/lp/translations/configure.zcml 2010-07-28 22:36:12 +0000
@@ -399,6 +399,16 @@
399 interface="lp.translations.interfaces.productserieslanguage.IProductSeriesLanguageSet"/>399 interface="lp.translations.interfaces.productserieslanguage.IProductSeriesLanguageSet"/>
400 </securedutility>400 </securedutility>
401401
402 <!-- TranslatedLanguage -->
403 <facet
404 facet="translations">
405 <class
406 class="lp.translations.model.translatedlanguage.POFilesByPOTemplates">
407 <allow
408 interface="lp.translations.interfaces.translatedlanguage.IPOFilesByPOTemplates"/>
409 </class>
410 </facet>
411
402 <!-- POTemplate -->412 <!-- POTemplate -->
403 <facet413 <facet
404 facet="translations">414 facet="translations">
@@ -430,6 +440,13 @@
430 provides="lp.translations.interfaces.translationcommonformat.ITranslationFileData"440 provides="lp.translations.interfaces.translationcommonformat.ITranslationFileData"
431 factory="lp.translations.model.potemplate.POTemplateToTranslationFileDataAdapter"/>441 factory="lp.translations.model.potemplate.POTemplateToTranslationFileDataAdapter"/>
432442
443 <!-- TranslationTemplatesCollection -->
444 <class
445 class="lp.translations.model.potemplate.TranslationTemplatesCollection">
446 <allow
447 interface="lp.translations.interfaces.potemplate.ITranslationTemplatesCollection"/>
448 </class>
449
433 <!-- POTemplateSet -->450 <!-- POTemplateSet -->
434451
435 <securedutility452 <securedutility
436453
=== modified file 'lib/lp/translations/interfaces/potemplate.py'
--- lib/lp/translations/interfaces/potemplate.py 2010-07-22 14:59:48 +0000
+++ lib/lp/translations/interfaces/potemplate.py 2010-07-28 22:36:12 +0000
@@ -781,5 +781,19 @@
781 exist for it.781 exist for it.
782 """782 """
783783
784class ITranslationTemplatesCollection(Interface):
785 """A `Collection` of `POTemplate`s."""
786
787 def joinOuterPOFile(language=None):
788 """Outer-join `POFile` into the collection.
789
790 :return: A `TranslationTemplatesCollection` with an added outer
791 join to `POFile`.
792 """
793
794 def select(*args):
795 """Return a ResultSet for this collection with values set to args."""
796
797
784# Monkey patch for circular import avoidance done in798# Monkey patch for circular import avoidance done in
785# _schema_circular_imports.py799# _schema_circular_imports.py
786800
=== modified file 'lib/lp/translations/interfaces/productserieslanguage.py'
--- lib/lp/translations/interfaces/productserieslanguage.py 2010-07-19 15:31:57 +0000
+++ lib/lp/translations/interfaces/productserieslanguage.py 2010-07-28 22:36:12 +0000
@@ -5,13 +5,13 @@
55
6from lazr.restful.fields import Reference6from lazr.restful.fields import Reference
77
8from zope.interface import Attribute, Interface8from zope.interface import Interface
9from zope.schema import (9from zope.schema import Choice, TextLine
10 Choice, Datetime, TextLine)
1110
12from canonical.launchpad import _11from canonical.launchpad import _
13from lp.translations.interfaces.pofile import IPOFile12from lp.translations.interfaces.pofile import IPOFile
14from lp.translations.interfaces.rosettastats import IRosettaStats13from lp.translations.interfaces.rosettastats import IRosettaStats
14from lp.translations.interfaces.translatedlanguage import ITranslatedLanguage
1515
16__metaclass__ = type16__metaclass__ = type
1717
@@ -21,13 +21,9 @@
21 ]21 ]
2222
2323
24class IProductSeriesLanguage(IRosettaStats):24class IProductSeriesLanguage(IRosettaStats, ITranslatedLanguage):
25 """Per-language statistics for a product series."""25 """Per-language statistics for a product series."""
2626
27 language = Choice(
28 title=_('Language to gather statistics for.'),
29 vocabulary='Language', required=True, readonly=True)
30
31 pofile = Reference(27 pofile = Reference(
32 title=_("A POFile if there is only one POTemplate for the series."),28 title=_("A POFile if there is only one POTemplate for the series."),
33 schema=IPOFile, required=False, readonly=True)29 schema=IPOFile, required=False, readonly=True)
@@ -41,27 +37,6 @@
41 title=_("Title for the per-language per-series page."),37 title=_("Title for the per-language per-series page."),
42 required=False)38 required=False)
4339
44 pofiles = Attribute("The set of pofiles in this distroseries for this "
45 "language. This includes only the real pofiles where translations "
46 "exist.")
47
48
49 last_changed_date = Datetime(
50 title=_('When this file was last changed.'))
51
52 def getPOFilesFor(potemplates):
53 """Return `POFiles` for each of `potemplates`, in the same order.
54
55 For any `POTemplate` that does not have a translation to the
56 required language, a `DummyPOFile` is provided.
57 """
58
59 def setCounts(total, imported, changed, new, unreviewed, last_changed):
60 """Set aggregated message counts for ProductSeriesLanguage."""
61
62 def recalculateCounts(total, imported, changed, new, unreviewed):
63 """Recalculate message counts for this ProductSeriesLanguage."""
64
6540
66class IProductSeriesLanguageSet(Interface):41class IProductSeriesLanguageSet(Interface):
67 """The set of productserieslanguages."""42 """The set of productserieslanguages."""
6843
=== added file 'lib/lp/translations/interfaces/translatedlanguage.py'
--- lib/lp/translations/interfaces/translatedlanguage.py 1970-01-01 00:00:00 +0000
+++ lib/lp/translations/interfaces/translatedlanguage.py 2010-07-28 22:36:12 +0000
@@ -0,0 +1,79 @@
1# Copyright 2010 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4# pylint: disable-msg=E0211,E0213
5
6from zope.interface import Attribute, Interface
7from zope.interface.common.sequence import IFiniteSequence
8from zope.schema import Datetime, Object
9
10from canonical.launchpad import _
11from lp.services.worlddata.interfaces.language import ILanguage
12from lp.translations.interfaces.potemplate import IHasTranslationTemplates
13from lp.registry.interfaces.person import IPerson
14
15__metaclass__ = type
16
17__all__ = [
18 'IPOFilesByPOTemplates',
19 'ITranslatedLanguage',
20 ]
21
22
23class ITranslatedLanguage(Interface):
24 """Interface for providing translations for context by language.
25
26 It expects `parent` to provide `IHasTranslationTemplates`.
27 """
28
29 language = Object(
30 title=_('Language to gather statistics and POFiles for.'),
31 schema=ILanguage)
32
33 parent = Object(
34 title=_('A parent with translation templates.'),
35 schema=IHasTranslationTemplates)
36
37 pofiles = Attribute(
38 _('Iterator over all POFiles for this context and language.'))
39
40 translation_statistics = Attribute(
41 _('A dict containing relevant aggregated statistics counts.'))
42
43 def setCounts(total, translated, new, changed, unreviewed):
44 """Set aggregated message counts for ITranslatedLanguage."""
45
46 def recalculateCounts():
47 """Recalculate message counts for this ITranslatedLanguage."""
48
49 last_changed_date = Datetime(
50 title=_('When was this translation last changed.'),
51 readonly=False, required=True)
52
53 last_translator = Object(
54 title=_('Last person that translated something in this context.'),
55 schema=IPerson)
56
57
58class IPOFilesByPOTemplates(IFiniteSequence):
59 """Iterate `IPOFile`s for (`ILanguage`, `ITranslationTemplateCollection`).
60
61 This is a wrapper for Storm ResultSet that enables optimized slicing
62 by doing it lazily on the query, thus allowing DummyPOFile objects
63 to be returned while still not doing more than one database query.
64
65 It subclasses `IFiniteSequence` so it can easily be used with the
66 BatchNavigator.
67 """
68
69 def __getitem__(selector):
70 """Get an element or slice of `IPOFile`s for given templates."""
71
72 def __getslice__(start, end):
73 """Deprecated, and implemented through __getitem__."""
74
75 def __iter__():
76 """Iterates over all `IPOFile`s for given templates."""
77
78 def __len__():
79 """Provides count of `IPOTemplate`s in a template collection."""
080
=== modified file 'lib/lp/translations/model/potemplate.py'
--- lib/lp/translations/model/potemplate.py 2010-07-23 19:44:16 +0000
+++ lib/lp/translations/model/potemplate.py 2010-07-28 22:36:12 +0000
@@ -1560,7 +1560,8 @@
1560 @property1560 @property
1561 def has_current_translation_templates(self):1561 def has_current_translation_templates(self):
1562 """See `IHasTranslationTemplates`."""1562 """See `IHasTranslationTemplates`."""
1563 return bool(self.getCurrentTranslationTemplates(just_ids=True).any())1563 return bool(
1564 self.getCurrentTranslationTemplates(just_ids=True).any())
15641565
1565 def getCurrentTranslationFiles(self, just_ids=False):1566 def getCurrentTranslationFiles(self, just_ids=False):
1566 """See `IHasTranslationTemplates`."""1567 """See `IHasTranslationTemplates`."""
@@ -1655,10 +1656,16 @@
1655 """1656 """
1656 return self.joinInner(POFile, POTemplate.id == POFile.potemplateID)1657 return self.joinInner(POFile, POTemplate.id == POFile.potemplateID)
16571658
1658 def joinOuterPOFile(self):1659 def joinOuterPOFile(self, language=None):
1659 """Outer-join `POFile` into the collection.1660 """Outer-join `POFile` into the collection.
16601661
1661 :return: A `TranslationTemplatesCollection` with an added outer1662 :return: A `TranslationTemplatesCollection` with an added outer
1662 join to `POFile`.1663 join to `POFile`.
1663 """1664 """
1664 return self.joinOuter(POFile, POTemplate.id == POFile.potemplateID)1665 if language is not None:
1666 return self.joinOuter(
1667 POFile, And(POTemplate.id == POFile.potemplateID,
1668 POFile.languageID == language.id))
1669 else:
1670 return self.joinOuter(
1671 POFile, POTemplate.id == POFile.potemplateID)
16651672
=== modified file 'lib/lp/translations/model/productserieslanguage.py'
--- lib/lp/translations/model/productserieslanguage.py 2010-07-19 15:38:51 +0000
+++ lib/lp/translations/model/productserieslanguage.py 2010-07-28 22:36:12 +0000
@@ -12,17 +12,13 @@
1212
13from zope.interface import implements13from zope.interface import implements
1414
15from storm.expr import Coalesce, Sum
16from storm.store import Store
17
18from lp.translations.utilities.rosettastats import RosettaStats15from lp.translations.utilities.rosettastats import RosettaStats
19from lp.translations.model.pofile import POFile16from lp.translations.model.translatedlanguage import TranslatedLanguageMixin
20from lp.translations.model.potemplate import get_pofiles_for, POTemplate
21from lp.translations.interfaces.productserieslanguage import (17from lp.translations.interfaces.productserieslanguage import (
22 IProductSeriesLanguage, IProductSeriesLanguageSet)18 IProductSeriesLanguage, IProductSeriesLanguageSet)
2319
2420
25class ProductSeriesLanguage(RosettaStats):21class ProductSeriesLanguage(RosettaStats, TranslatedLanguageMixin):
26 """See `IProductSeriesLanguage`."""22 """See `IProductSeriesLanguage`."""
27 implements(IProductSeriesLanguage)23 implements(IProductSeriesLanguage)
2824
@@ -30,56 +26,14 @@
30 assert 'en' != language.code, (26 assert 'en' != language.code, (
31 'English is not a translatable language.')27 'English is not a translatable language.')
32 RosettaStats.__init__(self)28 RosettaStats.__init__(self)
29 TranslatedLanguageMixin.__init__(self)
33 self.productseries = productseries30 self.productseries = productseries
31 self.parent = productseries
34 self.language = language32 self.language = language
35 self.variant = variant33 self.variant = variant
36 self.pofile = pofile34 self.pofile = pofile
37 self.id = 035 self.id = 0
38 self._last_changed_date = None36 self.last_changed_date = None
39
40 # Reset all cached counts.
41 self.setCounts()
42
43 def setCounts(self, total=0, imported=0, changed=0, new=0,
44 unreviewed=0, last_changed=None):
45 """See `IProductSeriesLanguage`."""
46 self._messagecount = total
47 # "currentcount" in RosettaStats conflicts our recent terminology
48 # and is closer to "imported" (except that it doesn't include
49 # "changed") translations.
50 self._currentcount = imported
51 self._updatescount = changed
52 self._rosettacount = new
53 self._unreviewed_count = unreviewed
54 if last_changed is not None:
55 self._last_changed_date = last_changed
56
57 def _getMessageCount(self):
58 store = Store.of(self.language)
59 query = store.find(Sum(POTemplate.messagecount),
60 POTemplate.productseries==self.productseries,
61 POTemplate.iscurrent==True)
62 total, = query
63 if total is None:
64 total = 0
65 return total
66
67 def recalculateCounts(self):
68 """See `IProductSeriesLanguage`."""
69 store = Store.of(self.language)
70 query = store.find(
71 (Coalesce(Sum(POFile.currentcount), 0),
72 Coalesce(Sum(POFile.updatescount), 0),
73 Coalesce(Sum(POFile.rosettacount), 0),
74 Coalesce(Sum(POFile.unreviewed_count), 0)),
75 POFile.language==self.language,
76 POFile.variant==None,
77 POFile.potemplate==POTemplate.id,
78 POTemplate.productseries==self.productseries,
79 POTemplate.iscurrent==True)
80 imported, changed, new, unreviewed = query[0]
81 self.setCounts(self._getMessageCount(), imported, changed,
82 new, unreviewed)
8337
84 @property38 @property
85 def title(self):39 def title(self):
@@ -90,46 +44,29 @@
90 self.productseries.displayname)44 self.productseries.displayname)
9145
92 def messageCount(self):46 def messageCount(self):
93 """See `IProductSeriesLanguage`."""47 """See `IRosettaStats`."""
94 return self._messagecount48 return self._translation_statistics['total_count']
9549
96 def currentCount(self, language=None):50 def currentCount(self, language=None):
97 """See `IProductSeriesLanguage`."""51 """See `IRosettaStats`."""
98 return self._currentcount52 translated = self._translation_statistics['translated_count']
53 current = translated - self.rosettaCount(language)
54 return current
9955
100 def updatesCount(self, language=None):56 def updatesCount(self, language=None):
101 """See `IProductSeriesLanguage`."""57 """See `IRosettaStats`."""
102 return self._updatescount58 return self._translation_statistics['changed_count']
10359
104 def rosettaCount(self, language=None):60 def rosettaCount(self, language=None):
105 """See `IProductSeriesLanguage`."""61 """See `IRosettaStats`."""
106 return self._rosettacount62 new = self._translation_statistics['new_count']
63 changed = self._translation_statistics['changed_count']
64 rosetta = new + changed
65 return rosetta
10766
108 def unreviewedCount(self):67 def unreviewedCount(self):
109 """See `IProductSeriesLanguage`."""68 """See `IRosettaStats`."""
110 return self._unreviewed_count69 return self._translation_statistics['unreviewed_count']
111
112 @property
113 def last_changed_date(self):
114 """See `IProductSeriesLanguage`."""
115 return self._last_changed_date
116
117 @property
118 def pofiles(self):
119 """See `IProductSeriesLanguage`."""
120 store = Store.of(self.language)
121 result = store.find(
122 POFile,
123 POFile.language==self.language,
124 POFile.variant==self.variant,
125 POFile.potemplate==POTemplate.id,
126 POTemplate.productseries==self.productseries,
127 POTemplate.iscurrent==True)
128 return result.order_by(['-priority'])
129
130 def getPOFilesFor(self, potemplates):
131 """See `IProductSeriesLanguage`."""
132 return get_pofiles_for(potemplates, self.language, self.variant)
13370
13471
135class ProductSeriesLanguageSet:72class ProductSeriesLanguageSet:
13673
=== added file 'lib/lp/translations/model/translatedlanguage.py'
--- lib/lp/translations/model/translatedlanguage.py 1970-01-01 00:00:00 +0000
+++ lib/lp/translations/model/translatedlanguage.py 2010-07-28 22:36:12 +0000
@@ -0,0 +1,132 @@
1# Copyright 2010 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4__all__ = ['TranslatedLanguageMixin']
5
6import pytz
7
8from zope.interface import implements
9
10from storm.expr import Coalesce, Desc, Max, Sum
11
12from lp.translations.interfaces.potemplate import IHasTranslationTemplates
13from lp.translations.interfaces.translatedlanguage import (
14 IPOFilesByPOTemplates, ITranslatedLanguage)
15from lp.translations.model.pofile import POFile
16from lp.translations.model.potemplate import POTemplate
17
18
19class POFilesByPOTemplates(object):
20 """See `IPOFilesByPOTemplates`."""
21 implements(IPOFilesByPOTemplates)
22
23 def __init__(self, templates_collection, language):
24 self.templates_collection = templates_collection
25 self.language = language
26
27 def _getDummyOrPOFile(self, potemplate, pofile):
28 if pofile is None:
29 return potemplate.getDummyPOFile(self.language,
30 check_for_existing=False)
31 else:
32 return pofile
33
34 def _getPOTemplatesAndPOFilesResultSet(self):
35 current_templates = self.templates_collection
36 pofiles = current_templates.joinOuterPOFile(self.language)
37 results = pofiles.select(POTemplate, POFile).order_by(
38 Desc(POTemplate.priority), POTemplate.name)
39 return results
40
41 def _getPOFilesForResultSet(self, resultset, selector=None):
42 pofiles_list = []
43 if selector is None:
44 results = resultset
45 else:
46 results = resultset[selector]
47 for potemplate, pofile in results:
48 pofiles_list.append(self._getDummyOrPOFile(potemplate, pofile))
49 return pofiles_list
50
51 def __getitem__(self, selector):
52 resultset = self._getPOTemplatesAndPOFilesResultSet()
53 if isinstance(selector, slice):
54 return self._getPOFilesForResultSet(resultset, selector)
55 else:
56 potemplate, pofile = resultset[selector]
57 return self._getDummyOrPOFile(potemplate, pofile)
58
59 def __iter__(self):
60 resultset = self._getPOTemplatesAndPOFilesResultSet()
61 for pofile in self._getPOFilesForResultSet(resultset):
62 yield pofile
63
64 def __len__(self):
65 return self.templates_collection.select(POTemplate).count()
66
67 def __nonzero__(self):
68 return bool(self.templates_collection.select(POTemplate).any())
69
70
71class TranslatedLanguageMixin(object):
72 """See `ITranslatedLanguage`."""
73 implements(ITranslatedLanguage)
74
75 language = None
76 parent = None
77
78 def __init__(self):
79 self.setCounts(total=0, translated=0, new=0, changed=0, unreviewed=0)
80
81 @property
82 def pofiles(self):
83 """See `ITranslatedLanguage`."""
84 assert IHasTranslationTemplates.providedBy(self.parent), (
85 "Parent object should implement `IHasTranslationTemplates`.")
86 current_templates = self.parent.getCurrentTemplatesCollection()
87 return POFilesByPOTemplates(current_templates, self.language)
88
89 @property
90 def translation_statistics(self):
91 """See `ITranslatedLanguage`."""
92 # This is a temporary translation statistics 'object' to allow
93 # smoother migration from IRosettaStats to something much nicer.
94 return self._translation_statistics
95
96 def setCounts(self, total, translated, new, changed, unreviewed):
97 """See `ITranslatedLanguage`."""
98 untranslated = total - translated
99 self._translation_statistics = {
100 'total_count': total,
101 'translated_count': translated,
102 'new_count': new,
103 'changed_count': changed,
104 'unreviewed_count': unreviewed,
105 'untranslated_count': untranslated,
106 }
107
108 def recalculateCounts(self):
109 """See `ITranslatedLanguage`."""
110 templates = self.parent.getCurrentTemplatesCollection()
111 pofiles = templates.joinOuterPOFile(self.language)
112 total_count_results = list(
113 pofiles.select(Coalesce(Sum(POTemplate.messagecount), 0),
114 Coalesce(Sum(POFile.currentcount), 0),
115 Coalesce(Sum(POFile.updatescount), 0),
116 Coalesce(Sum(POFile.rosettacount), 0),
117 Coalesce(Sum(POFile.unreviewed_count), 0),
118 Max(POFile.date_changed)))
119 total, imported, changed, rosetta, unreviewed, date_changed = (
120 total_count_results[0])
121 translated = imported + rosetta
122 new = rosetta - changed
123 self.setCounts(total, translated, new, changed, unreviewed)
124
125 # We have to add a timezone to the otherwise naive-datetime object
126 # (because we've gotten it using Max() aggregate function).
127 if date_changed is not None:
128 date_changed = date_changed.replace(tzinfo=pytz.UTC)
129 self.last_changed_date = date_changed
130
131 last_changed_date = None
132 last_translator = None
0133
=== modified file 'lib/lp/translations/tests/test_productserieslanguage.py'
--- lib/lp/translations/tests/test_productserieslanguage.py 2010-07-21 09:35:41 +0000
+++ lib/lp/translations/tests/test_productserieslanguage.py 2010-07-28 22:36:12 +0000
@@ -89,8 +89,11 @@
89 self.assertEquals(sr_psl.language, serbian)89 self.assertEquals(sr_psl.language, serbian)
90 self.assertEquals(sr_psl.pofile, None)90 self.assertEquals(sr_psl.pofile, None)
9191
92 # Only this POFile is returned by the `pofiles` property.92 # A POFile is returned where it exists, and a DummyPOFile where
93 self.assertEquals(list(sr_psl.pofiles), [pofile1])93 # it doesn't.
94 self.assertEquals(2, len(sr_psl.pofiles))
95 self.assertEquals(potemplate2, sr_psl.pofiles[0].potemplate)
96 self.assertEquals(pofile1, sr_psl.pofiles[1])
9497
95 # If we provide a POFile for the other template, `pofiles`98 # If we provide a POFile for the other template, `pofiles`
96 # returns both (ordered by decreasing priority).99 # returns both (ordered by decreasing priority).
@@ -173,8 +176,9 @@
173 self.productseries, self.language)176 self.productseries, self.language)
174 self.assertEquals(psl.messageCount(), 0)177 self.assertEquals(psl.messageCount(), 0)
175178
176 # So, we need to get it through productseries.productserieslanguages.179 # We explicitely ask for stats to be recalculated.
177 psl = self.productseries.productserieslanguages[0]180 psl.recalculateCounts()
181
178 self.assertPSLStatistics(psl,182 self.assertPSLStatistics(psl,
179 (pofile.messageCount(),183 (pofile.messageCount(),
180 pofile.translatedCount(),184 pofile.translatedCount(),
@@ -199,17 +203,14 @@
199 self.setPOFileStatistics(pofile2, 1, 1, 1, 1, pofile2.date_changed)203 self.setPOFileStatistics(pofile2, 1, 1, 1, 1, pofile2.date_changed)
200204
201 psl = self.productseries.productserieslanguages[0]205 psl = self.productseries.productserieslanguages[0]
202206 # We explicitely ask for stats to be recalculated.
203 # The psl.last_changed_date here is a naive datetime. So, for sake of207 psl.recalculateCounts()
204 # the tests, we should make pofile2 naive when checking if it matches
205 # the last calculated changed date, that should be the same as
206 # pofile2, created last.
207208
208 # Total is a sum of totals in both POTemplates (10+20).209 # Total is a sum of totals in both POTemplates (10+20).
209 # Translated is a sum of imported and rosetta translations,210 # Translated is a sum of imported and rosetta translations,
210 # which adds up as (4+3)+(1+1).211 # which adds up as (4+3)+(1+1).
211 self.assertPSLStatistics(psl, (30, 9, 5, 4, 3, 6,212 self.assertPSLStatistics(psl, (30, 9, 5, 4, 3, 6,
212 pofile2.date_changed.replace(tzinfo=None)))213 pofile2.date_changed))
213 self.assertPSLStatistics(psl, (214 self.assertPSLStatistics(psl, (
214 pofile1.messageCount() + pofile2.messageCount(),215 pofile1.messageCount() + pofile2.messageCount(),
215 pofile1.translatedCount() + pofile2.translatedCount(),216 pofile1.translatedCount() + pofile2.translatedCount(),
@@ -217,7 +218,7 @@
217 pofile1.rosettaCount() + pofile2.rosettaCount(),218 pofile1.rosettaCount() + pofile2.rosettaCount(),
218 pofile1.updatesCount() + pofile2.updatesCount(),219 pofile1.updatesCount() + pofile2.updatesCount(),
219 pofile1.unreviewedCount() + pofile2.unreviewedCount(),220 pofile1.unreviewedCount() + pofile2.unreviewedCount(),
220 pofile2.date_changed.replace(tzinfo=None)))221 pofile2.date_changed))
221222
222 def test_recalculateCounts(self):223 def test_recalculateCounts(self):
223 # Test that recalculateCounts works correctly.224 # Test that recalculateCounts works correctly.
@@ -236,13 +237,14 @@
236237
237 psl = self.psl_set.getProductSeriesLanguage(self.productseries,238 psl = self.psl_set.getProductSeriesLanguage(self.productseries,
238 self.language)239 self.language)
239 # recalculateCounts() doesn't recalculate the last changed date.240
240 psl.recalculateCounts()241 psl.recalculateCounts()
241 # Total is a sum of totals in both POTemplates (10+20).242 # Total is a sum of totals in both POTemplates (10+20).
242 # Translated is a sum of imported and rosetta translations,243 # Translated is a sum of imported and rosetta translations,
243 # which adds up as (1+3)+(1+1).244 # which adds up as (1+3)+(1+1).
245 # recalculateCounts() recalculates even the last changed date.
244 self.assertPSLStatistics(psl, (30, 6, 2, 4, 3, 5,246 self.assertPSLStatistics(psl, (30, 6, 2, 4, 3, 5,
245 None))247 pofile2.date_changed))
246248
247 def test_recalculateCounts_no_pofiles(self):249 def test_recalculateCounts_no_pofiles(self):
248 # Test that recalculateCounts works correctly even when there250 # Test that recalculateCounts works correctly even when there
249251
=== added file 'lib/lp/translations/tests/test_translatedlanguage.py'
--- lib/lp/translations/tests/test_translatedlanguage.py 1970-01-01 00:00:00 +0000
+++ lib/lp/translations/tests/test_translatedlanguage.py 2010-07-28 22:36:12 +0000
@@ -0,0 +1,462 @@
1# Copyright 2010 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4__metaclass__ = type
5
6from zope.component import getUtility
7from zope.interface.verify import verifyObject
8from zope.security.proxy import removeSecurityProxy
9
10from lp.translations.interfaces.productserieslanguage import (
11 IProductSeriesLanguageSet)
12from lp.translations.interfaces.translatedlanguage import ITranslatedLanguage
13from lp.translations.model.pofile import DummyPOFile
14from lp.testing import TestCaseWithFactory
15from canonical.testing import ZopelessDatabaseLayer
16
17
18class TestTranslatedLanguageMixin(TestCaseWithFactory):
19 """Test TranslatedLanguageMixin."""
20
21 layer = ZopelessDatabaseLayer
22
23 def setUp(self):
24 # Create a productseries that uses translations.
25 TestCaseWithFactory.setUp(self)
26 self.productseries = self.factory.makeProductSeries()
27 self.productseries.product.official_rosetta = True
28 self.parent = self.productseries
29 self.psl_set = getUtility(IProductSeriesLanguageSet)
30 self.language = self.factory.makeLanguage('sr@test')
31
32 def getTranslatedLanguage(self, language):
33 return self.psl_set.getProductSeriesLanguage(self.productseries,
34 language)
35
36 def addPOTemplate(self, number_of_potmsgsets=0, priority=0):
37 potemplate = self.factory.makePOTemplate(
38 productseries=self.productseries)
39 for sequence in range(number_of_potmsgsets):
40 self.factory.makePOTMsgSet(potemplate, sequence=sequence+1)
41 removeSecurityProxy(potemplate).messagecount = number_of_potmsgsets
42 potemplate.priority = priority
43 return potemplate
44
45 def test_interface(self):
46 translated_language = self.getTranslatedLanguage(self.language)
47 self.assertTrue(verifyObject(ITranslatedLanguage,
48 translated_language))
49
50 def test_language(self):
51 translated_language = self.getTranslatedLanguage(self.language)
52 self.assertEqual(self.language,
53 translated_language.language)
54
55 def test_parent(self):
56 translated_language = self.getTranslatedLanguage(self.language)
57 self.assertEqual(self.parent,
58 translated_language.parent)
59
60 def test_pofiles_notemplates(self):
61 translated_language = self.getTranslatedLanguage(self.language)
62 self.assertEqual([], list(translated_language.pofiles))
63
64 def test_pofiles_template_no_pofiles(self):
65 translated_language = self.getTranslatedLanguage(self.language)
66 potemplate = self.addPOTemplate()
67 dummy_pofile = potemplate.getDummyPOFile(self.language)
68 pofiles = list(translated_language.pofiles)
69 self.assertEqual(1, len(pofiles))
70
71 # When there are no actual PO files, we get a DummyPOFile object
72 # instead.
73 dummy_pofile = pofiles[0]
74 naked_dummy = removeSecurityProxy(dummy_pofile)
75 self.assertEqual(DummyPOFile, type(naked_dummy))
76 self.assertEqual(self.language, dummy_pofile.language)
77 self.assertEqual(potemplate, dummy_pofile.potemplate)
78
79 # Two queries get executed when listifying
80 # TranslatedLanguageMixin.pofiles: a len() does a count, and
81 # then all POTemplates and POFiles are fetched with the other.
82 self.assertStatementCount(2, list, translated_language.pofiles)
83
84 def test_pofiles_template_with_pofiles(self):
85 translated_language = self.getTranslatedLanguage(self.language)
86 potemplate = self.addPOTemplate()
87 pofile = self.factory.makePOFile(self.language.code, potemplate)
88 self.assertEqual([pofile], list(translated_language.pofiles))
89
90 # Two queries get executed when listifying
91 # TranslatedLanguageMixin.pofiles: a len() does a count, and
92 # then all POTemplates and POFiles are fetched with the other.
93 self.assertStatementCount(2, list, translated_language.pofiles)
94
95 def test_pofiles_two_templates(self):
96 translated_language = self.getTranslatedLanguage(self.language)
97 # Two templates with different priorities so they get sorted
98 # appropriately.
99 potemplate1 = self.addPOTemplate(priority=2)
100 pofile1 = self.factory.makePOFile(self.language.code, potemplate1)
101 potemplate2 = self.addPOTemplate(priority=1)
102 pofile2 = self.factory.makePOFile(self.language.code, potemplate2)
103 self.assertEqual([pofile1, pofile2],
104 list(translated_language.pofiles))
105
106 # Two queries get executed when listifying
107 # TranslatedLanguageMixin.pofiles: a len() does a count, and
108 # then all POTemplates and POFiles are fetched with the other.
109 self.assertStatementCount(2, list, translated_language.pofiles)
110
111 def test_pofiles_two_templates_one_dummy(self):
112 translated_language = self.getTranslatedLanguage(self.language)
113 # Two templates with different priorities so they get sorted
114 # appropriately.
115 potemplate1 = self.addPOTemplate(priority=2)
116 pofile1 = self.factory.makePOFile(self.language.code, potemplate1)
117 potemplate2 = self.addPOTemplate(priority=1)
118 pofiles = translated_language.pofiles
119 self.assertEqual(pofile1, pofiles[0])
120 dummy_pofile = removeSecurityProxy(pofiles[1])
121 self.assertEqual(DummyPOFile, type(dummy_pofile))
122
123 # Two queries get executed when listifying
124 # TranslatedLanguageMixin.pofiles: a len() does a count, and
125 # then all POTemplates and POFiles are fetched with the other.
126 self.assertStatementCount(2, list, translated_language.pofiles)
127
128 def test_pofiles_slicing(self):
129 # Slicing still works, and always does the same constant number
130 # of queries (1).
131 translated_language = self.getTranslatedLanguage(self.language)
132 # Three templates with different priorities so they get sorted
133 # appropriately.
134 potemplate1 = self.addPOTemplate(priority=2)
135 pofile1 = self.factory.makePOFile(self.language.code, potemplate1)
136 potemplate2 = self.addPOTemplate(priority=1)
137 pofile2 = self.factory.makePOFile(self.language.code, potemplate2)
138 potemplate3 = self.addPOTemplate(priority=0)
139
140 pofiles = translated_language.pofiles[0:2]
141 self.assertEqual([pofile1, pofile2], list(pofiles))
142
143 # Slicing executes only a single query.
144 get_slice = lambda of, start, end: list(of[start:end])
145 self.assertStatementCount(1, get_slice,
146 translated_language.pofiles, 1, 3)
147
148 def test_pofiles_slicing_dummies(self):
149 # Slicing includes DummyPOFiles.
150 translated_language = self.getTranslatedLanguage(self.language)
151 # Three templates with different priorities so they get sorted
152 # appropriately.
153 potemplate1 = self.addPOTemplate(priority=2)
154 pofile1 = self.factory.makePOFile(self.language.code, potemplate1)
155 potemplate2 = self.addPOTemplate(priority=1)
156 pofile2 = self.factory.makePOFile(self.language.code, potemplate2)
157 potemplate3 = self.addPOTemplate(priority=0)
158
159 pofiles = translated_language.pofiles[1:3]
160 self.assertEqual(pofile2, pofiles[0])
161 dummy_pofile = removeSecurityProxy(pofiles[1])
162 self.assertEqual(DummyPOFile, type(dummy_pofile))
163
164 def test_statistics_empty(self):
165 translated_language = self.getTranslatedLanguage(self.language)
166
167 expected = {
168 'total_count': 0,
169 'translated_count': 0,
170 'new_count': 0,
171 'changed_count': 0,
172 'unreviewed_count': 0,
173 'untranslated_count': 0,
174 }
175 self.assertEqual(expected,
176 translated_language.translation_statistics)
177
178 def test_setCounts_statistics(self):
179 translated_language = self.getTranslatedLanguage(self.language)
180
181 total = 5
182 translated = 4
183 new = 3
184 changed = 2
185 unreviewed = 1
186 untranslated = total - translated
187
188 translated_language.setCounts(
189 total, translated, new, changed, unreviewed)
190
191 expected = {
192 'total_count': total,
193 'translated_count': translated,
194 'new_count': new,
195 'changed_count': changed,
196 'unreviewed_count': unreviewed,
197 'untranslated_count': untranslated,
198 }
199 self.assertEqual(expected,
200 translated_language.translation_statistics)
201
202 def test_recalculateCounts_empty(self):
203 translated_language = self.getTranslatedLanguage(self.language)
204
205 translated_language.recalculateCounts()
206
207 expected = {
208 'total_count': 0,
209 'translated_count': 0,
210 'new_count': 0,
211 'changed_count': 0,
212 'unreviewed_count': 0,
213 'untranslated_count': 0,
214 }
215 self.assertEqual(expected,
216 translated_language.translation_statistics)
217
218 def test_recalculateCounts_total_one_pofile(self):
219 translated_language = self.getTranslatedLanguage(self.language)
220 potemplate = self.addPOTemplate(number_of_potmsgsets=5)
221 pofile = self.factory.makePOFile(self.language.code, potemplate)
222
223 translated_language.recalculateCounts()
224 self.assertEqual(
225 5, translated_language.translation_statistics['total_count'])
226
227 def test_recalculateCounts_total_two_pofiles(self):
228 translated_language = self.getTranslatedLanguage(self.language)
229 potemplate1 = self.addPOTemplate(number_of_potmsgsets=5)
230 pofile1 = self.factory.makePOFile(self.language.code, potemplate1)
231 potemplate2 = self.addPOTemplate(number_of_potmsgsets=3)
232 pofile2 = self.factory.makePOFile(self.language.code, potemplate2)
233
234 translated_language.recalculateCounts()
235 self.assertEqual(
236 5+3, translated_language.translation_statistics['total_count'])
237
238 def test_recalculateCounts_translated_one_pofile(self):
239 translated_language = self.getTranslatedLanguage(self.language)
240 potemplate = self.addPOTemplate(number_of_potmsgsets=5)
241 pofile = self.factory.makePOFile(self.language.code, potemplate)
242 naked_pofile = removeSecurityProxy(pofile)
243 # translated count is current + rosetta
244 naked_pofile.currentcount = 3
245 naked_pofile.rosettacount = 1
246
247 translated_language.recalculateCounts()
248 self.assertEqual(
249 4, translated_language.translation_statistics['translated_count'])
250
251 def test_recalculateCounts_translated_two_pofiles(self):
252 translated_language = self.getTranslatedLanguage(self.language)
253 potemplate1 = self.addPOTemplate(number_of_potmsgsets=5)
254 pofile1 = self.factory.makePOFile(self.language.code, potemplate1)
255 naked_pofile1 = removeSecurityProxy(pofile1)
256 # translated count is current + rosetta
257 naked_pofile1.currentcount = 3
258 naked_pofile1.rosettacount = 1
259
260 potemplate2 = self.addPOTemplate(number_of_potmsgsets=3)
261 pofile2 = self.factory.makePOFile(self.language.code, potemplate2)
262 naked_pofile2 = removeSecurityProxy(pofile2)
263 # translated count is current + rosetta
264 naked_pofile2.currentcount = 1
265 naked_pofile2.rosettacount = 1
266
267 translated_language.recalculateCounts()
268 self.assertEqual(
269 6, translated_language.translation_statistics['translated_count'])
270
271 def test_recalculateCounts_changed_one_pofile(self):
272 translated_language = self.getTranslatedLanguage(self.language)
273 potemplate = self.addPOTemplate(number_of_potmsgsets=5)
274 pofile = self.factory.makePOFile(self.language.code, potemplate)
275 naked_pofile = removeSecurityProxy(pofile)
276 # translated count is current + rosetta
277 naked_pofile.updatescount = 3
278
279 translated_language.recalculateCounts()
280 self.assertEqual(
281 3, translated_language.translation_statistics['changed_count'])
282
283 def test_recalculateCounts_changed_two_pofiles(self):
284 translated_language = self.getTranslatedLanguage(self.language)
285 potemplate1 = self.addPOTemplate(number_of_potmsgsets=5)
286 pofile1 = self.factory.makePOFile(self.language.code, potemplate1)
287 naked_pofile1 = removeSecurityProxy(pofile1)
288 naked_pofile1.updatescount = 3
289
290 potemplate2 = self.addPOTemplate(number_of_potmsgsets=3)
291 pofile2 = self.factory.makePOFile(self.language.code, potemplate2)
292 naked_pofile2 = removeSecurityProxy(pofile2)
293 naked_pofile2.updatescount = 1
294
295 translated_language.recalculateCounts()
296 self.assertEqual(
297 4, translated_language.translation_statistics['changed_count'])
298
299 def test_recalculateCounts_new_one_pofile(self):
300 translated_language = self.getTranslatedLanguage(self.language)
301 potemplate = self.addPOTemplate(number_of_potmsgsets=5)
302 pofile = self.factory.makePOFile(self.language.code, potemplate)
303 naked_pofile = removeSecurityProxy(pofile)
304 # new count is rosetta - changed
305 naked_pofile.rosettacount = 3
306 naked_pofile.updatescount = 1
307
308 translated_language.recalculateCounts()
309 self.assertEqual(
310 2, translated_language.translation_statistics['new_count'])
311
312 def test_recalculateCounts_new_two_pofiles(self):
313 translated_language = self.getTranslatedLanguage(self.language)
314 potemplate1 = self.addPOTemplate(number_of_potmsgsets=5)
315 pofile1 = self.factory.makePOFile(self.language.code, potemplate1)
316 naked_pofile1 = removeSecurityProxy(pofile1)
317 # new count is rosetta - changed
318 naked_pofile1.rosettacount = 3
319 naked_pofile1.updatescount = 1
320
321 potemplate2 = self.addPOTemplate(number_of_potmsgsets=3)
322 pofile2 = self.factory.makePOFile(self.language.code, potemplate2)
323 naked_pofile2 = removeSecurityProxy(pofile2)
324 # new count is rosetta - changed
325 naked_pofile2.rosettacount = 2
326 naked_pofile2.updatescount = 1
327
328 translated_language.recalculateCounts()
329 self.assertEqual(
330 3, translated_language.translation_statistics['new_count'])
331
332 def test_recalculateCounts_unreviewed_one_pofile(self):
333 translated_language = self.getTranslatedLanguage(self.language)
334 potemplate = self.addPOTemplate(number_of_potmsgsets=5)
335 pofile = self.factory.makePOFile(self.language.code, potemplate)
336 naked_pofile = removeSecurityProxy(pofile)
337 # translated count is current + rosetta
338 naked_pofile.unreviewed_count = 3
339
340 translated_language.recalculateCounts()
341 self.assertEqual(
342 3, translated_language.translation_statistics['unreviewed_count'])
343
344 def test_recalculateCounts_unreviewed_two_pofiles(self):
345 translated_language = self.getTranslatedLanguage(self.language)
346 potemplate1 = self.addPOTemplate(number_of_potmsgsets=5)
347 pofile1 = self.factory.makePOFile(self.language.code, potemplate1)
348 naked_pofile1 = removeSecurityProxy(pofile1)
349 naked_pofile1.unreviewed_count = 3
350
351 potemplate2 = self.addPOTemplate(number_of_potmsgsets=3)
352 pofile2 = self.factory.makePOFile(self.language.code, potemplate2)
353 naked_pofile2 = removeSecurityProxy(pofile2)
354 naked_pofile2.unreviewed_count = 1
355
356 translated_language.recalculateCounts()
357 self.assertEqual(
358 4, translated_language.translation_statistics['unreviewed_count'])
359
360 def test_recalculateCounts_one_pofile(self):
361 translated_language = self.getTranslatedLanguage(self.language)
362 potemplate = self.addPOTemplate(number_of_potmsgsets=5)
363 pofile = self.factory.makePOFile(self.language.code, potemplate)
364 naked_pofile = removeSecurityProxy(pofile)
365 # translated count is current + rosetta
366 naked_pofile.currentcount = 3
367 naked_pofile.rosettacount = 1
368 # Changed count is 'updatescount' on POFile.
369 # It has to be lower or equal to currentcount.
370 naked_pofile.updatescount = 1
371 # new is rosettacount-updatescount.
372 naked_pofile.newcount = 0
373 naked_pofile.unreviewed_count = 3
374
375 translated_language.recalculateCounts()
376
377 expected = {
378 'total_count': 5,
379 'translated_count': 4,
380 'new_count': 0,
381 'changed_count': 1,
382 'unreviewed_count': 3,
383 'untranslated_count': 1,
384 }
385 self.assertEqual(expected,
386 translated_language.translation_statistics)
387
388 def test_recalculateCounts_two_pofiles(self):
389 translated_language = self.getTranslatedLanguage(self.language)
390
391 # Set up one template with a single PO file.
392 potemplate1 = self.addPOTemplate(number_of_potmsgsets=5)
393 pofile1 = self.factory.makePOFile(self.language.code, potemplate1)
394 naked_pofile1 = removeSecurityProxy(pofile1)
395 # translated count is current + rosetta
396 naked_pofile1.currentcount = 2
397 naked_pofile1.rosettacount = 2
398 # Changed count is 'updatescount' on POFile.
399 # It has to be lower or equal to currentcount.
400 # new is rosettacount-updatescount.
401 naked_pofile1.updatescount = 1
402 naked_pofile1.unreviewed_count = 3
403
404 # Set up second template with a single PO file.
405 potemplate2 = self.addPOTemplate(number_of_potmsgsets=3)
406 pofile2 = self.factory.makePOFile(self.language.code, potemplate2)
407 naked_pofile2 = removeSecurityProxy(pofile2)
408 # translated count is current + rosetta
409 naked_pofile2.currentcount = 1
410 naked_pofile2.rosettacount = 2
411 # Changed count is 'updatescount' on POFile.
412 # It has to be lower or equal to currentcount.
413 # new is rosettacount-updatescount.
414 naked_pofile2.updatescount = 1
415 naked_pofile2.unreviewed_count = 1
416
417 translated_language.recalculateCounts()
418
419 expected = {
420 'total_count': 8,
421 'translated_count': 7,
422 'new_count': 2,
423 'changed_count': 2,
424 'unreviewed_count': 4,
425 'untranslated_count': 1,
426 }
427 self.assertEqual(expected,
428 translated_language.translation_statistics)
429
430 def test_recalculateCounts_two_templates_one_translation(self):
431 # Make sure recalculateCounts works even if a POFile is missing
432 # for one of the templates.
433 translated_language = self.getTranslatedLanguage(self.language)
434
435 # Set up one template with a single PO file.
436 potemplate1 = self.addPOTemplate(number_of_potmsgsets=5)
437 pofile1 = self.factory.makePOFile(self.language.code, potemplate1)
438 naked_pofile1 = removeSecurityProxy(pofile1)
439 # translated count is current + rosetta
440 naked_pofile1.currentcount = 2
441 naked_pofile1.rosettacount = 2
442 # Changed count is 'updatescount' on POFile.
443 # It has to be lower or equal to currentcount.
444 # new is rosettacount-updatescount.
445 naked_pofile1.updatescount = 1
446 naked_pofile1.unreviewed_count = 3
447
448 # Set up second template with a single PO file.
449 potemplate2 = self.addPOTemplate(number_of_potmsgsets=3)
450
451 translated_language.recalculateCounts()
452
453 expected = {
454 'total_count': 8,
455 'translated_count': 4,
456 'new_count': 1,
457 'changed_count': 1,
458 'unreviewed_count': 3,
459 'untranslated_count': 4,
460 }
461 self.assertEqual(expected,
462 translated_language.translation_statistics)
0463
=== modified file 'lib/lp/translations/tests/test_translationtemplatescollection.py'
--- lib/lp/translations/tests/test_translationtemplatescollection.py 2010-07-17 16:19:38 +0000
+++ lib/lp/translations/tests/test_translationtemplatescollection.py 2010-07-28 22:36:12 +0000
@@ -205,3 +205,22 @@
205 ]205 ]
206 self.assertContentEqual(206 self.assertContentEqual(
207 expected_outcome, joined.select(POTemplate, POFile))207 expected_outcome, joined.select(POTemplate, POFile))
208
209 def test_joinOuterPOFile_language(self):
210 trunk = self.factory.makeProduct().getSeries('trunk')
211 translated_template = self.factory.makePOTemplate(productseries=trunk)
212 untranslated_template = self.factory.makePOTemplate(
213 productseries=trunk)
214 nl = translated_template.newPOFile('nl')
215 de = translated_template.newPOFile('de')
216
217 collection = TranslationTemplatesCollection()
218 by_series = collection.restrictProductSeries(trunk)
219 joined = by_series.joinOuterPOFile(language=nl.language)
220
221 expected_outcome = [
222 (translated_template, nl),
223 (untranslated_template, None),
224 ]
225 self.assertContentEqual(
226 expected_outcome, joined.select(POTemplate, POFile))