← Back to team overview

launchpad-reviewers team mailing list archive

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

 

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

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)


= Remove unused field =

DistroSeriesLanguage has a translator_count which is not used anywhere. Get rid of it.

There are neither any tests for it.

(there is a translator_count on IRosettaApplication which *is* used, so I am not getting rid of that)
-- 
https://code.launchpad.net/~danilo/launchpad/translator_count_removal/+merge/30929
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~danilo/launchpad/translator_count_removal into lp:launchpad/devel.
=== modified file 'lib/lp/code/browser/tests/test_sourcepackagerecipe.py'
=== modified file 'lib/lp/code/model/tests/test_branch.py'
=== 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-26 12:07:51 +0000
@@ -465,12 +465,15 @@
 
             for language, pofile in ordered_results:
                 psl = ProductSeriesLanguage(self, language, pofile=pofile)
-                psl.setCounts(pofile.potemplate.messageCount(),
-                              pofile.currentCount(),
-                              pofile.updatesCount(),
-                              pofile.rosettaCount(),
-                              pofile.unreviewedCount(),
-                              pofile.date_changed)
+                total = pofile.potemplate.messageCount()
+                imported = pofile.currentCount()
+                changed = pofile.updatesCount()
+                rosetta = pofile.rosettaCount()
+                unreviewed = pofile.unreviewedCount()
+                translated = imported + rosetta
+                new = rosetta - changed
+                psl.setCounts(total, translated, new, changed, unreviewed)
+                psl.last_changed_date = pofile.date_changed
                 results.append(psl)
         else:
             # If there is more than one template, do a single
@@ -498,22 +501,15 @@
                 POTemplate.iscurrent==True,
                 Language.id!=english.id).group_by(Language)
 
-            # XXX: Ursinha 2009-11-02: The Max(POFile.date_changed) result
-            # here is a naive datetime. My guess is that it happens
-            # because UTC awareness is attibuted to the field in the POFile
-            # model class, and in this case the Max function deals directly
-            # with the value returned from the database without
-            # instantiating it.
-            # This seems to be irrelevant to what we're trying to achieve
-            # here, but making a note either way.
-
             ordered_results = query.order_by(['Language.englishname'])
 
-            for (language, imported, changed, new, unreviewed,
-                last_changed) in ordered_results:
+            for (language, imported, changed, rosetta, unreviewed,
+                 last_changed) in ordered_results:
                 psl = ProductSeriesLanguage(self, language)
-                psl.setCounts(
-                    total, imported, changed, new, unreviewed, last_changed)
+                translated = imported + rosetta
+                new = rosetta - changed
+                psl.setCounts(total, translated, new, changed, 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-26 06:21:45 +0000
+++ lib/lp/testing/factory.py	2010-07-26 12:07:51 +0000
@@ -158,7 +158,6 @@
     ANONYMOUS,
     login,
     login_as,
-    logout,
     run_with_login,
     temp_dir,
     time_counter,
@@ -840,7 +839,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,
@@ -1625,15 +1624,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-26 12:07:51 +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/productseries.py'
--- lib/lp/translations/browser/productseries.py	2010-07-22 14:59:48 +0000
+++ lib/lp/translations/browser/productseries.py	2010-07-26 12:07:51 +0000
@@ -370,10 +370,12 @@
                         productserieslang = (
                             productserieslangset.getProductSeriesLanguage(
                                 self.context, lang, pofile=pofile))
+                        productserieslang.recalculateCounts()
                     else:
                         productserieslang = (
                             productserieslangset.getProductSeriesLanguage(
                                 self.context, lang))
+                        productserieslang.recalculateCounts()
                     productserieslangs.append(
                         productserieslang)
 

=== 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-26 12:07:51 +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-26 12:07:51 +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/distroserieslanguage.py'
--- lib/lp/translations/interfaces/distroserieslanguage.py	2009-12-16 15:21:36 +0000
+++ lib/lp/translations/interfaces/distroserieslanguage.py	2010-07-26 12:07:51 +0000
@@ -35,9 +35,6 @@
         "language. This includes only the real pofiles where translations "
         "exist.")
 
-    translator_count = Attribute("The number of registered translators "
-        "for this language in the distribution.")
-
     contributor_count = Attribute("The number of contributors in total "
         "for this language in the distribution.")
 

=== 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-26 12:07:51 +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-26 12:07:51 +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-26 12:07:51 +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/distroserieslanguage.py'
--- lib/lp/translations/model/distroserieslanguage.py	2009-09-11 06:57:21 +0000
+++ lib/lp/translations/model/distroserieslanguage.py	2010-07-26 12:07:51 +0000
@@ -88,13 +88,6 @@
             distinct=True)
 
     @property
-    def translator_count(self):
-        translators = set()
-        for translator in self.translators:
-            translators = translators.union(translator.allmembers)
-        return len(translators)
-
-    @property
     def contributor_count(self):
         return self.contributorcount
 
@@ -156,7 +149,6 @@
         self.distroseries = distroseries
         self.messageCount = distroseries.messagecount
         self.dateupdated = datetime.now(tz=pytz.timezone('UTC'))
-        self.translator_count = 0
         self.contributor_count = 0
         self.title = '%s translations of %s %s' % (
             self.language.englishname,

=== 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-26 12:07:51 +0000
@@ -1560,7 +1560,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`."""
@@ -1655,10 +1656,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-26 12:07:51 +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-26 12:07:51 +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-26 12:07:51 +0000
@@ -89,8 +89,11 @@
         self.assertEquals(sr_psl.language, serbian)
         self.assertEquals(sr_psl.pofile, None)
 
-        # Only this POFile is returned by the `pofiles` property.
-        self.assertEquals(list(sr_psl.pofiles), [pofile1])
+        # A POFile is returned where it exists, and a DummyPOFile where
+        # it doesn't.
+        self.assertEquals(2, len(sr_psl.pofiles))
+        self.assertEquals(potemplate2, sr_psl.pofiles[0].potemplate)
+        self.assertEquals(pofile1, sr_psl.pofiles[1])
 
         # If we provide a POFile for the other template, `pofiles`
         # returns both (ordered by decreasing priority).
@@ -173,8 +176,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 +203,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 +218,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 +237,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-26 12:07:51 +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-26 12:07:51 +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))


Follow ups