← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~danilo/launchpad/translatedlanguage into lp:launchpad/devel

 

Данило Шеган has proposed merging lp:~danilo/launchpad/translatedlanguage into lp:launchpad/devel.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)


= 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)
-- 
https://code.launchpad.net/~danilo/launchpad/translatedlanguage/+merge/30788
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~danilo/launchpad/translatedlanguage into lp:launchpad/devel.
=== 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-23 15:41:30 +0000
@@ -469,8 +469,8 @@
                               pofile.currentCount(),
                               pofile.updatesCount(),
                               pofile.rosettaCount(),
-                              pofile.unreviewedCount(),
-                              pofile.date_changed)
+                              pofile.unreviewedCount())
+                psl.last_changed_date = pofile.date_changed
                 results.append(psl)
         else:
             # If there is more than one template, do a single
@@ -512,8 +512,8 @@
             for (language, imported, changed, new, unreviewed,
                 last_changed) in ordered_results:
                 psl = ProductSeriesLanguage(self, language)
-                psl.setCounts(
-                    total, imported, changed, new, unreviewed, last_changed)
+                psl.setCounts(total, imported, changed, new, unreviewed)
+                psl.last_changed_date = last_changed
                 results.append(psl)
 
         return results

=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py	2010-07-23 13:32:10 +0000
+++ lib/lp/testing/factory.py	2010-07-23 15:41:30 +0000
@@ -158,7 +158,6 @@
     ANONYMOUS,
     login,
     login_as,
-    logout,
     run_with_login,
     temp_dir,
     time_counter,
@@ -834,7 +833,7 @@
                 url = self.getUniqueURL()
         else:
             raise UnknownBranchTypeError(
-                'Unrecognized branch type: %r' % (branch_type,))
+                'Unrecognized branch type: %r' % (branch_type, ))
 
         namespace = get_branch_namespace(
             owner, product=product, distroseries=distroseries,
@@ -1619,15 +1618,28 @@
         syncUpdate(series)
         return series
 
-    def makeLanguage(self, language_code=None, name=None):
-        if language_code is None:
-            language_code = self.getUniqueString('lang')
-        if name is None:
-            name = "Language %s" % language_code
-
-        language_set = getUtility(ILanguageSet)
-        return language_set.createLanguage(language_code, name)
-
+<<<<<<< TREE
+    def makeLanguage(self, language_code=None, name=None):
+        if language_code is None:
+            language_code = self.getUniqueString('lang')
+        if name is None:
+            name = "Language %s" % language_code
+
+        language_set = getUtility(ILanguageSet)
+        return language_set.createLanguage(language_code, name)
+
+=======
+    def makeLanguage(self, language_code=None, name=None):
+        """Makes a language given the language_code and name."""
+        if language_code is None:
+            language_code = self.getUniqueString('lang')
+        if name is None:
+            name = "Language %s" % language_code
+
+        language_set = getUtility(ILanguageSet)
+        return language_set.createLanguage(language_code, name)
+
+>>>>>>> MERGE-SOURCE
     def makeLibraryFileAlias(self, filename=None, content=None,
                              content_type='text/plain', restricted=False,
                              expires=None):

=== 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-23 15:41:30 +0000
@@ -269,7 +269,7 @@
     <browser:url
         for="lp.translations.interfaces.productserieslanguage.IProductSeriesLanguage"
         path_expression="string:+lang/${language/code}"
-        attribute_to_parent="productseries"
+        attribute_to_parent="parent"
         rootsite="translations"/>
     <browser:navigation
         module="lp.translations.browser.serieslanguage"

=== 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-23 15:41:30 +0000
@@ -29,7 +29,7 @@
 
 
 class BaseSeriesLanguageView(LaunchpadView):
-    """View base class to render translation status for an 
+    """View base class to render translation status for an
     `IDistroSeries` and `IProductSeries`
 
     This class should not be directly instantiated.
@@ -46,12 +46,15 @@
         self.translationgroup = translationgroup
         self.form = self.request.form
 
-        self.batchnav = BatchNavigator(
-            self.series.getCurrentTranslationTemplates(),
-            self.request)
-
-        self.pofiles = self.context.getPOFilesFor(
-            self.batchnav.currentBatch())
+        if IDistroSeriesLanguage.providedBy(self.context):
+            self.batchnav = BatchNavigator(
+                self.series.getCurrentTranslationTemplates(),
+                self.request)
+            self.pofiles = self.context.getPOFilesFor(
+                self.batchnav.currentBatch())
+        else:
+            self.batchnav = BatchNavigator(self.context.pofiles, self.request)
+            self.pofiles = self.batchnav.currentBatch()
 
     @property
     def translation_group(self):
@@ -77,7 +80,7 @@
     @property
     def access_level_description(self):
         """Must not be called when there's no translation group."""
-        
+
         if is_read_only():
             return (
                 "No work can be done on these translations while Launchpad "

=== 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-23 15:41:30 +0000
@@ -399,6 +399,16 @@
             interface="lp.translations.interfaces.productserieslanguage.IProductSeriesLanguageSet"/>
     </securedutility>
 
+    <!-- TranslatedLanguage -->
+    <facet
+        facet="translations">
+        <class
+            class="lp.translations.model.translatedlanguage.POFilesByPOTemplates">
+            <allow
+                interface="lp.translations.interfaces.translatedlanguage.IPOFilesByPOTemplates"/>
+        </class>
+    </facet>
+
     <!-- POTemplate -->
     <facet
         facet="translations">
@@ -430,6 +440,13 @@
             provides="lp.translations.interfaces.translationcommonformat.ITranslationFileData"
             factory="lp.translations.model.potemplate.POTemplateToTranslationFileDataAdapter"/>
 
+        <!-- TranslationTemplatesCollection -->
+        <class
+            class="lp.translations.model.potemplate.TranslationTemplatesCollection">
+            <allow
+                interface="lp.translations.interfaces.potemplate.ITranslationTemplatesCollection"/>
+        </class>
+
         <!-- POTemplateSet -->
 
         <securedutility

=== 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-23 15:41:30 +0000
@@ -781,5 +781,19 @@
         exist for it.
         """
 
+class ITranslationTemplatesCollection(Interface):
+    """A `Collection` of `POTemplate`s."""
+
+    def joinOuterPOFile(language=None):
+        """Outer-join `POFile` into the collection.
+
+        :return: A `TranslationTemplatesCollection` with an added outer
+            join to `POFile`.
+        """
+
+    def select(*args):
+        """Return a ResultSet for this collection with values set to args."""
+
+
 # Monkey patch for circular import avoidance done in
 # _schema_circular_imports.py

=== 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-23 15:41:30 +0000
@@ -5,13 +5,13 @@
 
 from lazr.restful.fields import Reference
 
-from zope.interface import Attribute, Interface
-from zope.schema import (
-    Choice, Datetime, TextLine)
+from zope.interface import Interface
+from zope.schema import Choice, TextLine
 
 from canonical.launchpad import _
 from lp.translations.interfaces.pofile import IPOFile
 from lp.translations.interfaces.rosettastats import IRosettaStats
+from lp.translations.interfaces.translatedlanguage import ITranslatedLanguage
 
 __metaclass__ = type
 
@@ -21,13 +21,9 @@
     ]
 
 
-class IProductSeriesLanguage(IRosettaStats):
+class IProductSeriesLanguage(IRosettaStats, ITranslatedLanguage):
     """Per-language statistics for a product series."""
 
-    language = Choice(
-        title=_('Language to gather statistics for.'),
-        vocabulary='Language', required=True, readonly=True)
-
     pofile = Reference(
         title=_("A POFile if there is only one POTemplate for the series."),
         schema=IPOFile, required=False, readonly=True)
@@ -41,27 +37,6 @@
         title=_("Title for the per-language per-series page."),
         required=False)
 
-    pofiles = Attribute("The set of pofiles in this distroseries for this "
-        "language. This includes only the real pofiles where translations "
-        "exist.")
-
-
-    last_changed_date = Datetime(
-        title=_('When this file was last changed.'))
-
-    def getPOFilesFor(potemplates):
-        """Return `POFiles` for each of `potemplates`, in the same order.
-
-        For any `POTemplate` that does not have a translation to the
-        required language, a `DummyPOFile` is provided.
-        """
-
-    def setCounts(total, imported, changed, new, unreviewed, last_changed):
-        """Set aggregated message counts for ProductSeriesLanguage."""
-
-    def recalculateCounts(total, imported, changed, new, unreviewed):
-        """Recalculate message counts for this ProductSeriesLanguage."""
-
 
 class IProductSeriesLanguageSet(Interface):
     """The set of productserieslanguages."""

=== 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-23 15:41:30 +0000
@@ -0,0 +1,79 @@
+# Copyright 2009 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+# pylint: disable-msg=E0211,E0213
+
+from zope.interface import Attribute, Interface
+from zope.interface.common.sequence import IFiniteSequence
+from zope.schema import Datetime, Object
+
+from canonical.launchpad import _
+from lp.services.worlddata.interfaces.language import ILanguage
+from lp.translations.interfaces.potemplate import IHasTranslationTemplates
+from lp.registry.interfaces.person import IPerson
+
+__metaclass__ = type
+
+__all__ = [
+    'IPOFilesByPOTemplates',
+    'ITranslatedLanguage',
+    ]
+
+
+class ITranslatedLanguage(Interface):
+    """Interface for providing translations for context by language.
+
+    It expects `parent` to provide `IHasTranslationTemplates`.
+    """
+
+    language = Object(
+        title=_('Language to gather statistics and POFiles for.'),
+        schema=ILanguage)
+
+    parent = Object(
+        title=_('A parent with translation templates.'),
+        schema=IHasTranslationTemplates)
+
+    pofiles = Attribute(
+        _('Iterator over all POFiles for this context and language.'))
+
+    translation_statistics = Attribute(
+        _('A dict containing relevant aggregated statistics counts.'))
+
+    def setCounts(total, translated, new, changed, unreviewed):
+        """Set aggregated message counts for ITranslatedLanguage."""
+
+    def recalculateCounts():
+        """Recalculate message counts for this ITranslatedLanguage."""
+
+    last_changed_date = Datetime(
+        title=_('When was this translation last changed.'),
+        readonly=False, required=True)
+
+    last_translator = Object(
+        title=_('Last person that translated something in this context.'),
+        schema=IPerson)
+
+
+class IPOFilesByPOTemplates(IFiniteSequence):
+    """Iterate `IPOFile`s for (`ILanguage`, `ITranslationTemplateCollection`).
+
+    This is a wrapper for Storm ResultSet that enables optimized slicing
+    by doing it lazily on the query, thus allowing DummyPOFile objects
+    to be returned while still not doing more than one database query.
+
+    It subclasses `IFiniteSequence` so it can easily be used with the
+    BatchNavigator.
+    """
+
+    def __getitem__(selector):
+        """Get an element or slice of `IPOFile`s for given templates."""
+
+    def __getslice__(start, end):
+        """Deprecated, and implemented through __getitem__."""
+
+    def __iter__():
+        """Iterates over all `IPOFile`s for given templates."""
+
+    def __len__():
+        """Provides count of `IPOTemplate`s in a template collection."""

=== modified file 'lib/lp/translations/model/potemplate.py'
--- lib/lp/translations/model/potemplate.py	2010-07-22 14:59:48 +0000
+++ lib/lp/translations/model/potemplate.py	2010-07-23 15:41:30 +0000
@@ -1565,7 +1565,8 @@
     @property
     def has_current_translation_templates(self):
         """See `IHasTranslationTemplates`."""
-        return bool(self.getCurrentTranslationTemplates(just_ids=True).any())
+        return bool(
+            self.getCurrentTranslationTemplates(just_ids=True).any())
 
     def getCurrentTranslationFiles(self, just_ids=False):
         """See `IHasTranslationTemplates`."""
@@ -1660,10 +1661,16 @@
         """
         return self.joinInner(POFile, POTemplate.id == POFile.potemplateID)
 
-    def joinOuterPOFile(self):
+    def joinOuterPOFile(self, language=None):
         """Outer-join `POFile` into the collection.
 
         :return: A `TranslationTemplatesCollection` with an added outer
             join to `POFile`.
         """
-        return self.joinOuter(POFile, POTemplate.id == POFile.potemplateID)
+        if language is not None:
+            return self.joinOuter(
+                POFile, And(POTemplate.id == POFile.potemplateID,
+                            POFile.languageID == language.id))
+        else:
+            return self.joinOuter(
+                POFile, POTemplate.id == POFile.potemplateID)

=== 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-23 15:41:30 +0000
@@ -12,17 +12,13 @@
 
 from zope.interface import implements
 
-from storm.expr import Coalesce, Sum
-from storm.store import Store
-
 from lp.translations.utilities.rosettastats import RosettaStats
-from lp.translations.model.pofile import POFile
-from lp.translations.model.potemplate import get_pofiles_for, POTemplate
+from lp.translations.model.translatedlanguage import TranslatedLanguageMixin
 from lp.translations.interfaces.productserieslanguage import (
     IProductSeriesLanguage, IProductSeriesLanguageSet)
 
 
-class ProductSeriesLanguage(RosettaStats):
+class ProductSeriesLanguage(RosettaStats, TranslatedLanguageMixin):
     """See `IProductSeriesLanguage`."""
     implements(IProductSeriesLanguage)
 
@@ -30,56 +26,14 @@
         assert 'en' != language.code, (
             'English is not a translatable language.')
         RosettaStats.__init__(self)
+        TranslatedLanguageMixin.__init__(self)
         self.productseries = productseries
+        self.parent = productseries
         self.language = language
         self.variant = variant
         self.pofile = pofile
         self.id = 0
-        self._last_changed_date = None
-
-        # Reset all cached counts.
-        self.setCounts()
-
-    def setCounts(self, total=0, imported=0, changed=0, new=0,
-                  unreviewed=0, last_changed=None):
-        """See `IProductSeriesLanguage`."""
-        self._messagecount = total
-        # "currentcount" in RosettaStats conflicts our recent terminology
-        # and is closer to "imported" (except that it doesn't include
-        # "changed") translations.
-        self._currentcount = imported
-        self._updatescount = changed
-        self._rosettacount = new
-        self._unreviewed_count = unreviewed
-        if last_changed is not None:
-            self._last_changed_date = last_changed
-
-    def _getMessageCount(self):
-        store = Store.of(self.language)
-        query = store.find(Sum(POTemplate.messagecount),
-                           POTemplate.productseries==self.productseries,
-                           POTemplate.iscurrent==True)
-        total, = query
-        if total is None:
-            total = 0
-        return total
-
-    def recalculateCounts(self):
-        """See `IProductSeriesLanguage`."""
-        store = Store.of(self.language)
-        query = store.find(
-            (Coalesce(Sum(POFile.currentcount), 0),
-             Coalesce(Sum(POFile.updatescount), 0),
-             Coalesce(Sum(POFile.rosettacount), 0),
-             Coalesce(Sum(POFile.unreviewed_count), 0)),
-            POFile.language==self.language,
-            POFile.variant==None,
-            POFile.potemplate==POTemplate.id,
-            POTemplate.productseries==self.productseries,
-            POTemplate.iscurrent==True)
-        imported, changed, new, unreviewed = query[0]
-        self.setCounts(self._getMessageCount(), imported, changed,
-                       new, unreviewed)
+        self.last_changed_date = None
 
     @property
     def title(self):
@@ -90,46 +44,29 @@
             self.productseries.displayname)
 
     def messageCount(self):
-        """See `IProductSeriesLanguage`."""
-        return self._messagecount
+        """See `IRosettaStats`."""
+        return self._translation_statistics['total_count']
 
     def currentCount(self, language=None):
-        """See `IProductSeriesLanguage`."""
-        return self._currentcount
+        """See `IRosettaStats`."""
+        translated = self._translation_statistics['translated_count']
+        current = translated - self.rosettaCount(language)
+        return current
 
     def updatesCount(self, language=None):
-        """See `IProductSeriesLanguage`."""
-        return self._updatescount
+        """See `IRosettaStats`."""
+        return self._translation_statistics['changed_count']
 
     def rosettaCount(self, language=None):
-        """See `IProductSeriesLanguage`."""
-        return self._rosettacount
+        """See `IRosettaStats`."""
+        new = self._translation_statistics['new_count']
+        changed = self._translation_statistics['changed_count']
+        rosetta = new + changed
+        return rosetta
 
     def unreviewedCount(self):
-        """See `IProductSeriesLanguage`."""
-        return self._unreviewed_count
-
-    @property
-    def last_changed_date(self):
-        """See `IProductSeriesLanguage`."""
-        return self._last_changed_date
-
-    @property
-    def pofiles(self):
-        """See `IProductSeriesLanguage`."""
-        store = Store.of(self.language)
-        result = store.find(
-            POFile,
-            POFile.language==self.language,
-            POFile.variant==self.variant,
-            POFile.potemplate==POTemplate.id,
-            POTemplate.productseries==self.productseries,
-            POTemplate.iscurrent==True)
-        return result.order_by(['-priority'])
-
-    def getPOFilesFor(self, potemplates):
-        """See `IProductSeriesLanguage`."""
-        return get_pofiles_for(potemplates, self.language, self.variant)
+        """See `IRosettaStats`."""
+        return self._translation_statistics['unreviewed_count']
 
 
 class ProductSeriesLanguageSet:

=== 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-23 15:41:30 +0000
@@ -0,0 +1,132 @@
+# Copyright 2010 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+__all__ = ['TranslatedLanguageMixin']
+
+import pytz
+
+from zope.interface import implements
+
+from storm.expr import Coalesce, Desc, Max, Sum
+
+from lp.translations.interfaces.potemplate import IHasTranslationTemplates
+from lp.translations.interfaces.translatedlanguage import (
+    IPOFilesByPOTemplates, ITranslatedLanguage)
+from lp.translations.model.pofile import POFile
+from lp.translations.model.potemplate import POTemplate
+
+
+class POFilesByPOTemplates(object):
+    """See `IPOFilesByPOTemplates`."""
+    implements(IPOFilesByPOTemplates)
+
+    def __init__(self, templates_collection, language):
+        self.templates_collection = templates_collection
+        self.language = language
+
+    def _getDummyOrPOFile(self, potemplate, pofile):
+        if pofile is None:
+            return potemplate.getDummyPOFile(self.language,
+                                             check_for_existing=False)
+        else:
+            return pofile
+
+    def _getPOTemplatesAndPOFilesResultSet(self):
+        current_templates = self.templates_collection
+        pofiles = current_templates.joinOuterPOFile(self.language)
+        results = pofiles.select(POTemplate, POFile).order_by(
+            Desc(POTemplate.priority), POTemplate.name)
+        return results
+
+    def _getPOFilesForResultSet(self, resultset, selector=None):
+        pofiles_list = []
+        if selector is None:
+            results = resultset
+        else:
+            results = resultset[selector]
+        for potemplate, pofile in results:
+            pofiles_list.append(self._getDummyOrPOFile(potemplate, pofile))
+        return pofiles_list
+
+    def __getitem__(self, selector):
+        resultset = self._getPOTemplatesAndPOFilesResultSet()
+        if isinstance(selector, slice):
+            return self._getPOFilesForResultSet(resultset, selector)
+        else:
+            potemplate, pofile = resultset[selector]
+            return self._getDummyOrPOFile(potemplate, pofile)
+
+    def __iter__(self):
+        resultset = self._getPOTemplatesAndPOFilesResultSet()
+        for pofile in self._getPOFilesForResultSet(resultset):
+            yield pofile
+
+    def __len__(self):
+        return self.templates_collection.select(POTemplate).count()
+
+    def __nonzero__(self):
+        return bool(self.templates_collection.select(POTemplate).any())
+
+
+class TranslatedLanguageMixin(object):
+    """See `ITranslatedLanguage`."""
+    implements(ITranslatedLanguage)
+
+    language = None
+    parent = None
+
+    def __init__(self):
+        self.setCounts(total=0, translated=0, new=0, changed=0, unreviewed=0)
+
+    @property
+    def pofiles(self):
+        """See `ITranslatedLanguage`."""
+        assert IHasTranslationTemplates.providedBy(self.parent), (
+            "Parent object should implement `IHasTranslationTemplates`.")
+        current_templates = self.parent.getCurrentTemplatesCollection()
+        return POFilesByPOTemplates(current_templates, self.language)
+
+    @property
+    def translation_statistics(self):
+        """See `ITranslatedLanguage`."""
+        # This is a temporary translation statistics 'object' to allow
+        # smoother migration from IRosettaStats to something much nicer.
+        return self._translation_statistics
+
+    def setCounts(self, total, translated, new, changed, unreviewed):
+        """See `ITranslatedLanguage`."""
+        untranslated = total - translated
+        self._translation_statistics = {
+            'total_count': total,
+            'translated_count': translated,
+            'new_count': new,
+            'changed_count': changed,
+            'unreviewed_count': unreviewed,
+            'untranslated_count': untranslated,
+            }
+
+    def recalculateCounts(self):
+        """See `ITranslatedLanguage`."""
+        templates = self.parent.getCurrentTemplatesCollection()
+        pofiles = templates.joinOuterPOFile(self.language)
+        total_count_results = list(
+            pofiles.select(Coalesce(Sum(POTemplate.messagecount), 0),
+                           Coalesce(Sum(POFile.currentcount), 0),
+                           Coalesce(Sum(POFile.updatescount), 0),
+                           Coalesce(Sum(POFile.rosettacount), 0),
+                           Coalesce(Sum(POFile.unreviewed_count), 0),
+                           Max(POFile.date_changed)))
+        total, imported, changed, rosetta, unreviewed, date_changed = (
+            total_count_results[0])
+        translated = imported + rosetta
+        new = rosetta - changed
+        self.setCounts(total, translated, new, changed, unreviewed)
+
+        # We have to add a timezone to the otherwise naive-datetime object
+        # (because we've gotten it using Max() aggregate function).
+        if date_changed is not None:
+            date_changed = date_changed.replace(tzinfo=pytz.UTC)
+        self.last_changed_date = date_changed
+
+    last_changed_date = None
+    last_translator = None

=== 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-23 15:41:30 +0000
@@ -173,8 +173,9 @@
             self.productseries, self.language)
         self.assertEquals(psl.messageCount(), 0)
 
-        # So, we need to get it through productseries.productserieslanguages.
-        psl = self.productseries.productserieslanguages[0]
+        # We explicitely ask for stats to be recalculated.
+        psl.recalculateCounts()
+
         self.assertPSLStatistics(psl,
                                  (pofile.messageCount(),
                                   pofile.translatedCount(),
@@ -199,17 +200,14 @@
         self.setPOFileStatistics(pofile2, 1, 1, 1, 1, pofile2.date_changed)
 
         psl = self.productseries.productserieslanguages[0]
-
-        # The psl.last_changed_date here is a naive datetime. So, for sake of
-        # the tests, we should make pofile2 naive when checking if it matches
-        # the last calculated changed date, that should be the same as
-        # pofile2, created last.
+        # We explicitely ask for stats to be recalculated.
+        psl.recalculateCounts()
 
         # Total is a sum of totals in both POTemplates (10+20).
         # Translated is a sum of imported and rosetta translations,
         # which adds up as (4+3)+(1+1).
         self.assertPSLStatistics(psl, (30, 9, 5, 4, 3, 6,
-            pofile2.date_changed.replace(tzinfo=None)))
+            pofile2.date_changed))
         self.assertPSLStatistics(psl, (
             pofile1.messageCount() + pofile2.messageCount(),
             pofile1.translatedCount() + pofile2.translatedCount(),
@@ -217,7 +215,7 @@
             pofile1.rosettaCount() + pofile2.rosettaCount(),
             pofile1.updatesCount() + pofile2.updatesCount(),
             pofile1.unreviewedCount() + pofile2.unreviewedCount(),
-            pofile2.date_changed.replace(tzinfo=None)))
+            pofile2.date_changed))
 
     def test_recalculateCounts(self):
         # Test that recalculateCounts works correctly.
@@ -236,13 +234,14 @@
 
         psl = self.psl_set.getProductSeriesLanguage(self.productseries,
                                                     self.language)
-        # recalculateCounts() doesn't recalculate the last changed date.
+
         psl.recalculateCounts()
         # Total is a sum of totals in both POTemplates (10+20).
         # Translated is a sum of imported and rosetta translations,
         # which adds up as (1+3)+(1+1).
+        # recalculateCounts() recalculates even the last changed date.
         self.assertPSLStatistics(psl, (30, 6, 2, 4, 3, 5,
-            None))
+            pofile2.date_changed))
 
     def test_recalculateCounts_no_pofiles(self):
         # Test that recalculateCounts works correctly even when there

=== 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-23 15:41:30 +0000
@@ -0,0 +1,462 @@
+# Copyright 2010 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+__metaclass__ = type
+
+from zope.component import getUtility
+from zope.interface.verify import verifyObject
+from zope.security.proxy import removeSecurityProxy
+
+from lp.translations.interfaces.productserieslanguage import (
+    IProductSeriesLanguageSet)
+from lp.translations.interfaces.translatedlanguage import ITranslatedLanguage
+from lp.translations.model.pofile import DummyPOFile
+from lp.testing import TestCaseWithFactory
+from canonical.testing import ZopelessDatabaseLayer
+
+
+class TestTranslatedLanguageMixin(TestCaseWithFactory):
+    """Test TranslatedLanguageMixin."""
+
+    layer = ZopelessDatabaseLayer
+
+    def setUp(self):
+        # Create a productseries that uses translations.
+        TestCaseWithFactory.setUp(self)
+        self.productseries = self.factory.makeProductSeries()
+        self.productseries.product.official_rosetta = True
+        self.parent = self.productseries
+        self.psl_set = getUtility(IProductSeriesLanguageSet)
+        self.language = self.factory.makeLanguage('sr@test')
+
+    def getTranslatedLanguage(self, language):
+        return self.psl_set.getProductSeriesLanguage(self.productseries,
+                                                     language)
+
+    def addPOTemplate(self, number_of_potmsgsets=0, priority=0):
+        potemplate = self.factory.makePOTemplate(
+            productseries=self.productseries)
+        for sequence in range(number_of_potmsgsets):
+            self.factory.makePOTMsgSet(potemplate, sequence=sequence+1)
+        removeSecurityProxy(potemplate).messagecount = number_of_potmsgsets
+        potemplate.priority = priority
+        return potemplate
+
+    def test_interface(self):
+        translated_language = self.getTranslatedLanguage(self.language)
+        self.assertTrue(verifyObject(ITranslatedLanguage,
+                                     translated_language))
+
+    def test_language(self):
+        translated_language = self.getTranslatedLanguage(self.language)
+        self.assertEqual(self.language,
+                         translated_language.language)
+
+    def test_parent(self):
+        translated_language = self.getTranslatedLanguage(self.language)
+        self.assertEqual(self.parent,
+                         translated_language.parent)
+
+    def test_pofiles_notemplates(self):
+        translated_language = self.getTranslatedLanguage(self.language)
+        self.assertEqual([], list(translated_language.pofiles))
+
+    def test_pofiles_template_no_pofiles(self):
+        translated_language = self.getTranslatedLanguage(self.language)
+        potemplate = self.addPOTemplate()
+        dummy_pofile = potemplate.getDummyPOFile(self.language)
+        pofiles = list(translated_language.pofiles)
+        self.assertEqual(1, len(pofiles))
+
+        # When there are no actual PO files, we get a DummyPOFile object
+        # instead.
+        dummy_pofile = pofiles[0]
+        naked_dummy = removeSecurityProxy(dummy_pofile)
+        self.assertEqual(DummyPOFile, type(naked_dummy))
+        self.assertEqual(self.language, dummy_pofile.language)
+        self.assertEqual(potemplate, dummy_pofile.potemplate)
+
+        # Two queries get executed when listifying
+        # TranslatedLanguageMixin.pofiles: a len() does a count, and
+        # then all POTemplates and POFiles are fetched with the other.
+        self.assertStatementCount(2, list, translated_language.pofiles)
+
+    def test_pofiles_template_with_pofiles(self):
+        translated_language = self.getTranslatedLanguage(self.language)
+        potemplate = self.addPOTemplate()
+        pofile = self.factory.makePOFile(self.language.code, potemplate)
+        self.assertEqual([pofile], list(translated_language.pofiles))
+
+        # Two queries get executed when listifying
+        # TranslatedLanguageMixin.pofiles: a len() does a count, and
+        # then all POTemplates and POFiles are fetched with the other.
+        self.assertStatementCount(2, list, translated_language.pofiles)
+
+    def test_pofiles_two_templates(self):
+        translated_language = self.getTranslatedLanguage(self.language)
+        # Two templates with different priorities so they get sorted
+        # appropriately.
+        potemplate1 = self.addPOTemplate(priority=2)
+        pofile1 = self.factory.makePOFile(self.language.code, potemplate1)
+        potemplate2 = self.addPOTemplate(priority=1)
+        pofile2 = self.factory.makePOFile(self.language.code, potemplate2)
+        self.assertEqual([pofile1, pofile2],
+                         list(translated_language.pofiles))
+
+        # Two queries get executed when listifying
+        # TranslatedLanguageMixin.pofiles: a len() does a count, and
+        # then all POTemplates and POFiles are fetched with the other.
+        self.assertStatementCount(2, list, translated_language.pofiles)
+
+    def test_pofiles_two_templates_one_dummy(self):
+        translated_language = self.getTranslatedLanguage(self.language)
+        # Two templates with different priorities so they get sorted
+        # appropriately.
+        potemplate1 = self.addPOTemplate(priority=2)
+        pofile1 = self.factory.makePOFile(self.language.code, potemplate1)
+        potemplate2 = self.addPOTemplate(priority=1)
+        pofiles = translated_language.pofiles
+        self.assertEqual(pofile1, pofiles[0])
+        dummy_pofile = removeSecurityProxy(pofiles[1])
+        self.assertEqual(DummyPOFile, type(dummy_pofile))
+
+        # Two queries get executed when listifying
+        # TranslatedLanguageMixin.pofiles: a len() does a count, and
+        # then all POTemplates and POFiles are fetched with the other.
+        self.assertStatementCount(2, list, translated_language.pofiles)
+
+    def test_pofiles_slicing(self):
+        # Slicing still works, and always does the same constant number
+        # of queries (1).
+        translated_language = self.getTranslatedLanguage(self.language)
+        # Three templates with different priorities so they get sorted
+        # appropriately.
+        potemplate1 = self.addPOTemplate(priority=2)
+        pofile1 = self.factory.makePOFile(self.language.code, potemplate1)
+        potemplate2 = self.addPOTemplate(priority=1)
+        pofile2 = self.factory.makePOFile(self.language.code, potemplate2)
+        potemplate3 = self.addPOTemplate(priority=0)
+
+        pofiles = translated_language.pofiles[0:2]
+        self.assertEqual([pofile1, pofile2], list(pofiles))
+
+        # Slicing executes only a single query.
+        get_slice = lambda of, start, end: list(of[start:end])
+        self.assertStatementCount(1, get_slice,
+                                  translated_language.pofiles, 1, 3)
+
+    def test_pofiles_slicing_dummies(self):
+        # Slicing includes DummyPOFiles.
+        translated_language = self.getTranslatedLanguage(self.language)
+        # Three templates with different priorities so they get sorted
+        # appropriately.
+        potemplate1 = self.addPOTemplate(priority=2)
+        pofile1 = self.factory.makePOFile(self.language.code, potemplate1)
+        potemplate2 = self.addPOTemplate(priority=1)
+        pofile2 = self.factory.makePOFile(self.language.code, potemplate2)
+        potemplate3 = self.addPOTemplate(priority=0)
+
+        pofiles = translated_language.pofiles[1:3]
+        self.assertEqual(pofile2, pofiles[0])
+        dummy_pofile = removeSecurityProxy(pofiles[1])
+        self.assertEqual(DummyPOFile, type(dummy_pofile))
+
+    def test_statistics_empty(self):
+        translated_language = self.getTranslatedLanguage(self.language)
+
+        expected = {
+            'total_count': 0,
+            'translated_count': 0,
+            'new_count': 0,
+            'changed_count': 0,
+            'unreviewed_count': 0,
+            'untranslated_count': 0,
+            }
+        self.assertEqual(expected,
+                         translated_language.translation_statistics)
+
+    def test_setCounts_statistics(self):
+        translated_language = self.getTranslatedLanguage(self.language)
+
+        total = 5
+        translated = 4
+        new = 3
+        changed = 2
+        unreviewed = 1
+        untranslated = total - translated
+
+        translated_language.setCounts(
+            total, translated, new, changed, unreviewed)
+
+        expected = {
+            'total_count': total,
+            'translated_count': translated,
+            'new_count': new,
+            'changed_count': changed,
+            'unreviewed_count': unreviewed,
+            'untranslated_count': untranslated,
+            }
+        self.assertEqual(expected,
+                         translated_language.translation_statistics)
+
+    def test_recalculateCounts_empty(self):
+        translated_language = self.getTranslatedLanguage(self.language)
+
+        translated_language.recalculateCounts()
+
+        expected = {
+            'total_count': 0,
+            'translated_count': 0,
+            'new_count': 0,
+            'changed_count': 0,
+            'unreviewed_count': 0,
+            'untranslated_count': 0,
+            }
+        self.assertEqual(expected,
+                         translated_language.translation_statistics)
+
+    def test_recalculateCounts_total_one_pofile(self):
+        translated_language = self.getTranslatedLanguage(self.language)
+        potemplate = self.addPOTemplate(number_of_potmsgsets=5)
+        pofile = self.factory.makePOFile(self.language.code, potemplate)
+
+        translated_language.recalculateCounts()
+        self.assertEqual(
+            5, translated_language.translation_statistics['total_count'])
+
+    def test_recalculateCounts_total_two_pofiles(self):
+        translated_language = self.getTranslatedLanguage(self.language)
+        potemplate1 = self.addPOTemplate(number_of_potmsgsets=5)
+        pofile1 = self.factory.makePOFile(self.language.code, potemplate1)
+        potemplate2 = self.addPOTemplate(number_of_potmsgsets=3)
+        pofile2 = self.factory.makePOFile(self.language.code, potemplate2)
+
+        translated_language.recalculateCounts()
+        self.assertEqual(
+            5+3, translated_language.translation_statistics['total_count'])
+
+    def test_recalculateCounts_translated_one_pofile(self):
+        translated_language = self.getTranslatedLanguage(self.language)
+        potemplate = self.addPOTemplate(number_of_potmsgsets=5)
+        pofile = self.factory.makePOFile(self.language.code, potemplate)
+        naked_pofile = removeSecurityProxy(pofile)
+        # translated count is current + rosetta
+        naked_pofile.currentcount = 3
+        naked_pofile.rosettacount = 1
+
+        translated_language.recalculateCounts()
+        self.assertEqual(
+            4, translated_language.translation_statistics['translated_count'])
+
+    def test_recalculateCounts_translated_two_pofiles(self):
+        translated_language = self.getTranslatedLanguage(self.language)
+        potemplate1 = self.addPOTemplate(number_of_potmsgsets=5)
+        pofile1 = self.factory.makePOFile(self.language.code, potemplate1)
+        naked_pofile1 = removeSecurityProxy(pofile1)
+        # translated count is current + rosetta
+        naked_pofile1.currentcount = 3
+        naked_pofile1.rosettacount = 1
+
+        potemplate2 = self.addPOTemplate(number_of_potmsgsets=3)
+        pofile2 = self.factory.makePOFile(self.language.code, potemplate2)
+        naked_pofile2 = removeSecurityProxy(pofile2)
+        # translated count is current + rosetta
+        naked_pofile2.currentcount = 1
+        naked_pofile2.rosettacount = 1
+
+        translated_language.recalculateCounts()
+        self.assertEqual(
+            6, translated_language.translation_statistics['translated_count'])
+
+    def test_recalculateCounts_changed_one_pofile(self):
+        translated_language = self.getTranslatedLanguage(self.language)
+        potemplate = self.addPOTemplate(number_of_potmsgsets=5)
+        pofile = self.factory.makePOFile(self.language.code, potemplate)
+        naked_pofile = removeSecurityProxy(pofile)
+        # translated count is current + rosetta
+        naked_pofile.updatescount = 3
+
+        translated_language.recalculateCounts()
+        self.assertEqual(
+            3, translated_language.translation_statistics['changed_count'])
+
+    def test_recalculateCounts_changed_two_pofiles(self):
+        translated_language = self.getTranslatedLanguage(self.language)
+        potemplate1 = self.addPOTemplate(number_of_potmsgsets=5)
+        pofile1 = self.factory.makePOFile(self.language.code, potemplate1)
+        naked_pofile1 = removeSecurityProxy(pofile1)
+        naked_pofile1.updatescount = 3
+
+        potemplate2 = self.addPOTemplate(number_of_potmsgsets=3)
+        pofile2 = self.factory.makePOFile(self.language.code, potemplate2)
+        naked_pofile2 = removeSecurityProxy(pofile2)
+        naked_pofile2.updatescount = 1
+
+        translated_language.recalculateCounts()
+        self.assertEqual(
+            4, translated_language.translation_statistics['changed_count'])
+
+    def test_recalculateCounts_new_one_pofile(self):
+        translated_language = self.getTranslatedLanguage(self.language)
+        potemplate = self.addPOTemplate(number_of_potmsgsets=5)
+        pofile = self.factory.makePOFile(self.language.code, potemplate)
+        naked_pofile = removeSecurityProxy(pofile)
+        # new count is rosetta - changed
+        naked_pofile.rosettacount = 3
+        naked_pofile.updatescount = 1
+
+        translated_language.recalculateCounts()
+        self.assertEqual(
+            2, translated_language.translation_statistics['new_count'])
+
+    def test_recalculateCounts_new_two_pofiles(self):
+        translated_language = self.getTranslatedLanguage(self.language)
+        potemplate1 = self.addPOTemplate(number_of_potmsgsets=5)
+        pofile1 = self.factory.makePOFile(self.language.code, potemplate1)
+        naked_pofile1 = removeSecurityProxy(pofile1)
+        # new count is rosetta - changed
+        naked_pofile1.rosettacount = 3
+        naked_pofile1.updatescount = 1
+
+        potemplate2 = self.addPOTemplate(number_of_potmsgsets=3)
+        pofile2 = self.factory.makePOFile(self.language.code, potemplate2)
+        naked_pofile2 = removeSecurityProxy(pofile2)
+        # new count is rosetta - changed
+        naked_pofile2.rosettacount = 2
+        naked_pofile2.updatescount = 1
+
+        translated_language.recalculateCounts()
+        self.assertEqual(
+            3, translated_language.translation_statistics['new_count'])
+
+    def test_recalculateCounts_unreviewed_one_pofile(self):
+        translated_language = self.getTranslatedLanguage(self.language)
+        potemplate = self.addPOTemplate(number_of_potmsgsets=5)
+        pofile = self.factory.makePOFile(self.language.code, potemplate)
+        naked_pofile = removeSecurityProxy(pofile)
+        # translated count is current + rosetta
+        naked_pofile.unreviewed_count = 3
+
+        translated_language.recalculateCounts()
+        self.assertEqual(
+            3, translated_language.translation_statistics['unreviewed_count'])
+
+    def test_recalculateCounts_unreviewed_two_pofiles(self):
+        translated_language = self.getTranslatedLanguage(self.language)
+        potemplate1 = self.addPOTemplate(number_of_potmsgsets=5)
+        pofile1 = self.factory.makePOFile(self.language.code, potemplate1)
+        naked_pofile1 = removeSecurityProxy(pofile1)
+        naked_pofile1.unreviewed_count = 3
+
+        potemplate2 = self.addPOTemplate(number_of_potmsgsets=3)
+        pofile2 = self.factory.makePOFile(self.language.code, potemplate2)
+        naked_pofile2 = removeSecurityProxy(pofile2)
+        naked_pofile2.unreviewed_count = 1
+
+        translated_language.recalculateCounts()
+        self.assertEqual(
+            4, translated_language.translation_statistics['unreviewed_count'])
+
+    def test_recalculateCounts_one_pofile(self):
+        translated_language = self.getTranslatedLanguage(self.language)
+        potemplate = self.addPOTemplate(number_of_potmsgsets=5)
+        pofile = self.factory.makePOFile(self.language.code, potemplate)
+        naked_pofile = removeSecurityProxy(pofile)
+        # translated count is current + rosetta
+        naked_pofile.currentcount = 3
+        naked_pofile.rosettacount = 1
+        # Changed count is 'updatescount' on POFile.
+        # It has to be lower or equal to currentcount.
+        naked_pofile.updatescount = 1
+        # new is rosettacount-updatescount.
+        naked_pofile.newcount = 0
+        naked_pofile.unreviewed_count = 3
+
+        translated_language.recalculateCounts()
+
+        expected = {
+            'total_count': 5,
+            'translated_count': 4,
+            'new_count': 0,
+            'changed_count': 1,
+            'unreviewed_count': 3,
+            'untranslated_count': 1,
+            }
+        self.assertEqual(expected,
+                         translated_language.translation_statistics)
+
+    def test_recalculateCounts_two_pofiles(self):
+        translated_language = self.getTranslatedLanguage(self.language)
+
+        # Set up one template with a single PO file.
+        potemplate1 = self.addPOTemplate(number_of_potmsgsets=5)
+        pofile1 = self.factory.makePOFile(self.language.code, potemplate1)
+        naked_pofile1 = removeSecurityProxy(pofile1)
+        # translated count is current + rosetta
+        naked_pofile1.currentcount = 2
+        naked_pofile1.rosettacount = 2
+        # Changed count is 'updatescount' on POFile.
+        # It has to be lower or equal to currentcount.
+        # new is rosettacount-updatescount.
+        naked_pofile1.updatescount = 1
+        naked_pofile1.unreviewed_count = 3
+
+        # Set up second template with a single PO file.
+        potemplate2 = self.addPOTemplate(number_of_potmsgsets=3)
+        pofile2 = self.factory.makePOFile(self.language.code, potemplate2)
+        naked_pofile2 = removeSecurityProxy(pofile2)
+        # translated count is current + rosetta
+        naked_pofile2.currentcount = 1
+        naked_pofile2.rosettacount = 2
+        # Changed count is 'updatescount' on POFile.
+        # It has to be lower or equal to currentcount.
+        # new is rosettacount-updatescount.
+        naked_pofile2.updatescount = 1
+        naked_pofile2.unreviewed_count = 1
+
+        translated_language.recalculateCounts()
+
+        expected = {
+            'total_count': 8,
+            'translated_count': 7,
+            'new_count': 2,
+            'changed_count': 2,
+            'unreviewed_count': 4,
+            'untranslated_count': 1,
+            }
+        self.assertEqual(expected,
+                         translated_language.translation_statistics)
+
+    def test_recalculateCounts_two_templates_one_translation(self):
+        # Make sure recalculateCounts works even if a POFile is missing
+        # for one of the templates.
+        translated_language = self.getTranslatedLanguage(self.language)
+
+        # Set up one template with a single PO file.
+        potemplate1 = self.addPOTemplate(number_of_potmsgsets=5)
+        pofile1 = self.factory.makePOFile(self.language.code, potemplate1)
+        naked_pofile1 = removeSecurityProxy(pofile1)
+        # translated count is current + rosetta
+        naked_pofile1.currentcount = 2
+        naked_pofile1.rosettacount = 2
+        # Changed count is 'updatescount' on POFile.
+        # It has to be lower or equal to currentcount.
+        # new is rosettacount-updatescount.
+        naked_pofile1.updatescount = 1
+        naked_pofile1.unreviewed_count = 3
+
+        # Set up second template with a single PO file.
+        potemplate2 = self.addPOTemplate(number_of_potmsgsets=3)
+
+        translated_language.recalculateCounts()
+
+        expected = {
+            'total_count': 8,
+            'translated_count': 4,
+            'new_count': 1,
+            'changed_count': 1,
+            'unreviewed_count': 3,
+            'untranslated_count': 4,
+            }
+        self.assertEqual(expected,
+                         translated_language.translation_statistics)

=== 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-23 15:41:30 +0000
@@ -205,3 +205,22 @@
             ]
         self.assertContentEqual(
             expected_outcome, joined.select(POTemplate, POFile))
+
+    def test_joinOuterPOFile_language(self):
+        trunk = self.factory.makeProduct().getSeries('trunk')
+        translated_template = self.factory.makePOTemplate(productseries=trunk)
+        untranslated_template = self.factory.makePOTemplate(
+            productseries=trunk)
+        nl = translated_template.newPOFile('nl')
+        de = translated_template.newPOFile('de')
+
+        collection = TranslationTemplatesCollection()
+        by_series = collection.restrictProductSeries(trunk)
+        joined = by_series.joinOuterPOFile(language=nl.language)
+
+        expected_outcome = [
+            (translated_template, nl),
+            (untranslated_template, None),
+            ]
+        self.assertContentEqual(
+            expected_outcome, joined.select(POTemplate, POFile))