launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #00240
[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/30758
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 12:27:52 +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-22 14:06:45 +0000
+++ lib/lp/testing/factory.py 2010-07-23 12:27:52 +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,6 +1618,16 @@
syncUpdate(series)
return series
+ 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)
+
def makeLibraryFileAlias(self, filename=None, content=None,
content_type='text/plain', restricted=False,
expires=None):
=== modified file 'lib/lp/testing/tests/test_factory.py'
--- lib/lp/testing/tests/test_factory.py 2010-07-22 14:06:45 +0000
+++ lib/lp/testing/tests/test_factory.py 2010-07-23 12:27:52 +0000
@@ -14,7 +14,11 @@
from canonical.testing.layers import DatabaseFunctionalLayer
from lp.code.enums import CodeImportReviewStatus
from lp.testing import TestCaseWithFactory
+<<<<<<< TREE
from lp.testing.factory import is_security_proxied_or_harmless
+=======
+from lp.services.worlddata.interfaces.language import ILanguage
+>>>>>>> MERGE-SOURCE
class TestFactory(TestCaseWithFactory):
@@ -34,6 +38,32 @@
code_import = self.factory.makeCodeImport(review_status=status)
self.assertEqual(status, code_import.review_status)
+ def test_makeLanguage(self):
+ # Without parameters, makeLanguage creates a language with code
+ # starting with 'lang'.
+ language = self.factory.makeLanguage()
+ self.assertTrue(ILanguage.providedBy(language))
+ self.assertTrue(language.code.startswith('lang'))
+ # And name is constructed from code as 'Language %(code)s'.
+ self.assertEquals('Language %s' % language.code,
+ language.englishname)
+
+ def test_makeLanguage_with_code(self):
+ # With language code passed in, that's used for the language.
+ language = self.factory.makeLanguage('sr@test')
+ self.assertEquals('sr@test', language.code)
+ # And name is constructed from code as 'Language %(code)s'.
+ self.assertEquals('Language sr@test', language.englishname)
+
+ def test_makeLanguage_with_name(self):
+ # Language name can be passed in to makeLanguage (useful for
+ # use in page tests).
+ language = self.factory.makeLanguage(name='Test language')
+ self.assertTrue(ILanguage.providedBy(language))
+ self.assertTrue(language.code.startswith('lang'))
+ # And name is constructed from code as 'Language %(code)s'.
+ self.assertEquals('Test language', language.englishname)
+
def test_loginAsAnyone(self):
# Login as anyone logs you in as any user.
person = self.factory.loginAsAnyone()
=== 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 12:27:52 +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/potemplate.py'
--- lib/lp/translations/browser/potemplate.py 2010-07-15 09:41:27 +0000
+++ lib/lp/translations/browser/potemplate.py 2010-07-23 12:27:52 +0000
@@ -55,6 +55,7 @@
ITranslationImporter)
from lp.translations.interfaces.translationimportqueue import (
ITranslationImportQueue)
+from lp.services.worlddata.interfaces.language import ILanguageSet
from canonical.launchpad.webapp import (
action, canonical_url, enabled_with_permission, GetitemNavigation,
LaunchpadView, LaunchpadEditFormView, Link, Navigation, NavigationMenu,
@@ -90,7 +91,9 @@
elif self.request.method in ['GET', 'HEAD']:
# It's just a query, get a fake one so we don't create new
# POFiles just because someone is browsing the web.
- return self.context.getDummyPOFile(name, requester=user)
+ language = getUtility(ILanguageSet).getLanguageByCode(name)
+ return self.context.getDummyPOFile(language, requester=user,
+ check_for_existing=False)
else:
# It's a POST.
# XXX CarlosPerelloMarin 2006-04-20 bug=40275: We should
=== modified file 'lib/lp/translations/browser/productseries.py'
--- lib/lp/translations/browser/productseries.py 2010-07-19 13:30:29 +0000
+++ lib/lp/translations/browser/productseries.py 2010-07-23 12:27:52 +0000
@@ -366,7 +366,7 @@
pot = self.context.getCurrentTranslationTemplates()[0]
pofile = pot.getPOFileByLang(lang.code)
if pofile is None:
- pofile = pot.getDummyPOFile(lang.code)
+ pofile = pot.getDummyPOFile(lang)
productserieslang = (
productserieslangset.getProductSeriesLanguage(
self.context, lang, pofile=pofile))
=== 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 12:27:52 +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/browser/tests/pofile-views.txt'
--- lib/lp/translations/browser/tests/pofile-views.txt 2010-07-16 16:51:52 +0000
+++ lib/lp/translations/browser/tests/pofile-views.txt 2010-07-23 12:27:52 +0000
@@ -11,11 +11,12 @@
... TranslationMessage, DummyTranslationMessage)
>>> from lp.translations.interfaces.translationimportqueue import (
... ITranslationImportQueue)
+ >>> from lp.translations.publisher import TranslationsLayer
>>> from lp.translations.interfaces.potemplate import IPOTemplateSet
>>> from lp.registry.interfaces.distribution import IDistributionSet
>>> from lp.registry.interfaces.sourcepackagename import (
... ISourcePackageNameSet)
- >>> from lp.translations.publisher import TranslationsLayer
+ >>> from lp.services.worlddata.interfaces.language import ILanguageSet
All the tests will be submitted as coming from the No Privilege person.
@@ -57,7 +58,8 @@
This time, we are going to see what happens if we get an IPOFile without
the plural form information.
- >>> pofile_tlh = potemplate.getDummyPOFile('tlh')
+ >>> language_tlh = getUtility(ILanguageSet).getLanguageByCode('tlh')
+ >>> pofile_tlh = potemplate.getDummyPOFile(language_tlh)
>>> form = {'show': 'all' }
>>> pofile_view = create_view(
... pofile_tlh, '+translate', form=form, layer=TranslationsLayer)
=== modified file 'lib/lp/translations/browser/tests/translationmessage-views.txt'
--- lib/lp/translations/browser/tests/translationmessage-views.txt 2010-07-16 16:51:52 +0000
+++ lib/lp/translations/browser/tests/translationmessage-views.txt 2010-07-23 12:27:52 +0000
@@ -8,6 +8,7 @@
>>> from lp.translations.model.translationmessage import (
... TranslationMessage)
>>> from lp.translations.publisher import TranslationsLayer
+ >>> from lp.services.worlddata.interfaces.language import ILanguageSet
>>> from canonical.launchpad.webapp import canonical_url
All the tests will be submitted as comming from Kurem, an editor for the POFile
@@ -23,8 +24,8 @@
>>> translationmessage = TranslationMessage.get(1)
>>> pofile = POFile.get(1)
- >>> pofile_tlh = pofile.potemplate.getDummyPOFile(
- ... 'tlh')
+ >>> language_tlh = getUtility(ILanguageSet).getLanguageByCode('tlh')
+ >>> pofile_tlh = pofile.potemplate.getDummyPOFile(language_tlh)
>>> potmsgset = pofile_tlh.potemplate.getPOTMsgSetByMsgIDText(
... u'evolution addressbook')
>>> current_translationmessage = (
=== 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 12:27:52 +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/doc/pofile.txt'
--- lib/lp/translations/doc/pofile.txt 2010-02-01 15:06:20 +0000
+++ lib/lp/translations/doc/pofile.txt 2010-07-23 12:27:52 +0000
@@ -25,7 +25,9 @@
Get Xhosa translation
>>> pofile = potemplate.getPOFileByLang('xh')
- >>> dummy_pofile = potemplate.getDummyPOFile('pt_BR')
+ >>> language_pt_BR = getUtility(
+ ... ILanguageSet).getLanguageByCode('pt_BR')
+ >>> dummy_pofile = potemplate.getDummyPOFile(language_pt_BR)
Both implement the IPOFile interface:
@@ -97,7 +99,8 @@
>>> print potemplate.getPOFileByLang('es').getFullLanguageCode()
es
- >>> print potemplate.getDummyPOFile('sr', variant=u'Latn'
+ >>> language_sr = getUtility(ILanguageSet).getLanguageByCode('sr')
+ >>> print potemplate.getDummyPOFile(language_sr, variant=u'Latn'
... ).getFullLanguageCode()
sr@Latn
@@ -110,7 +113,7 @@
>>> print potemplate.getPOFileByLang('es').getFullLanguageName()
Spanish
- >>> print potemplate.getDummyPOFile('sr', variant=u'Latn'
+ >>> print potemplate.getDummyPOFile(language_sr, variant=u'Latn'
... ).getFullLanguageName()
Serbian ("Latn" variant)
@@ -439,7 +442,8 @@
Now, we get an IPOFile that does not have a translation team assigned.
- >>> pofile_cy = potemplate.getDummyPOFile('cy')
+ >>> language_cy = getUtility(ILanguageSet).getLanguageByCode('cy')
+ >>> pofile_cy = potemplate.getDummyPOFile(language_cy)
Valentina Commissari is not a translator for this language and does not
have permissions.
@@ -608,7 +612,7 @@
>>> serbian = getUtility(ILanguageSet)['sr']
>>> serbian.pluralforms
3
- >>> evolution_sr = evolution_pot.getDummyPOFile(serbian.code)
+ >>> evolution_sr = evolution_pot.getDummyPOFile(serbian)
>>> evolution_sr.plural_forms
3
@@ -618,7 +622,7 @@
>>> divehi = getUtility(ILanguageSet)['dv']
>>> print divehi.pluralforms
None
- >>> evolution_dv = evolution_pot.getDummyPOFile(divehi.code)
+ >>> evolution_dv = evolution_pot.getDummyPOFile(divehi)
>>> evolution_dv.plural_forms
2
=== modified file 'lib/lp/translations/doc/potemplate.txt'
--- lib/lp/translations/doc/potemplate.txt 2010-04-26 16:00:31 +0000
+++ lib/lp/translations/doc/potemplate.txt 2010-07-23 12:27:52 +0000
@@ -228,13 +228,25 @@
== getPOFileByPath ==
-We can get an IPOFile inside a templage based on its path.
+We can get an IPOFile inside a template based on its path.
>>> pofile = potemplate.getPOFileByPath('es.po')
>>> print pofile.title
Spanish (es) translation of evolution-2.2 in Evolution trunk
+== getDummyPOFile ==
+
+To get an IPOFile object even for languages which don't have a translation
+of this template, we use the getDummyPOFile method, passing in the language.
+
+ >>> xx_language = factory.makeLanguage(
+ ... 'xx@test', name='Test language')
+ >>> xx_pofile = potemplate.getDummyPOFile(xx_language)
+ >>> print xx_pofile.title
+ Test language (xx@test) translation of evolution-2.2 in Evolution trunk
+
+
== newPOFile ==
The Portuguese translation has not been started yet; therefore,
=== modified file 'lib/lp/translations/doc/potmsgset.txt'
--- lib/lp/translations/doc/potmsgset.txt 2010-03-06 06:08:21 +0000
+++ lib/lp/translations/doc/potmsgset.txt 2010-07-23 12:27:52 +0000
@@ -445,7 +445,10 @@
An empty translation does not need to exist in the database. If not,
a DummyPOFile is used instead.
- >>> pt_BR_dummypofile = evolution_potemplate.getDummyPOFile('pt_BR')
+ >>> language_pt_BR = getUtility(
+ ... ILanguageSet).getLanguageByCode('pt_BR')
+ >>> pt_BR_dummypofile = evolution_potemplate.getDummyPOFile(
+ ... language_pt_BR)
We get a POTMsgSet and verify it's a singular form:
@@ -475,7 +478,8 @@
Using another dummy pofile we'll get a POTMsgset that's not a singular
form:
- >>> apa_dummypofile = evolution_potemplate.getDummyPOFile('apa')
+ >>> language_apa = getUtility(ILanguageSet).getLanguageByCode('apa')
+ >>> apa_dummypofile = evolution_potemplate.getDummyPOFile(language_apa)
>>> plural_potmsgset = apa_dummypofile.potemplate.getPOTMsgSetByMsgIDText(
... u'%d contact', u'%d contacts')
>>> print apa_dummypofile.language.code
@@ -496,7 +500,8 @@
We can guess the pluralforms for this language through ILanguage.pluralforms:
- >>> ru_dummypofile = evolution_potemplate.getDummyPOFile('ru')
+ >>> language_ru = getUtility(ILanguageSet).getLanguageByCode('ru')
+ >>> ru_dummypofile = evolution_potemplate.getDummyPOFile(language_ru)
>>> ru_dummy_current = plural_potmsgset.getCurrentDummyTranslationMessage(
... evolution_potemplate, ru_dummypofile.language)
@@ -821,7 +826,8 @@
... distroseries=ubuntu_hoary).getPOTemplateByName('man')
>>> potmsgset_untranslated = pmount_man_template.getPOTMsgSetByMsgIDText(
... 'test man page')
- >>> pofile = pmount_man_template.getDummyPOFile('es')
+ >>> language_es = getUtility(ILanguageSet).getLanguageByCode('es')
+ >>> pofile = pmount_man_template.getDummyPOFile(language_es)
>>> print pofile.title
Spanish (es) translation of man in Ubuntu Hoary package "pmount"
=== modified file 'lib/lp/translations/interfaces/potemplate.py'
--- lib/lp/translations/interfaces/potemplate.py 2010-07-20 12:33:43 +0000
+++ lib/lp/translations/interfaces/potemplate.py 2010-07-23 12:27:52 +0000
@@ -425,7 +425,8 @@
loops when creating a new IPOTemplate.
"""
- def getDummyPOFile(language_code, variant=None, requester=None):
+ def getDummyPOFile(language, variant=None, requester=None,
+ check_for_existing=True):
"""Return a DummyPOFile if there isn't already a persistent `IPOFile`
Raise `LanguageNotFound` if the language does not exist in the
@@ -435,7 +436,8 @@
only create a POFile when you actually need to store data.
We should not have already a POFile for the given language_code and
- variant.
+ variant: if check_for_existing is set to False, no check will be
+ done for this.
"""
def createPOTMsgSetFromMsgIDs(msgid_singular, msgid_plural=None,
@@ -779,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 12:27:52 +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 12:27:52 +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-20 18:19:08 +0000
+++ lib/lp/translations/model/potemplate.py 2010-07-23 12:27:52 +0000
@@ -793,14 +793,15 @@
return pofile
- def getDummyPOFile(self, language_code, variant=None, requester=None):
+ def getDummyPOFile(self, language, variant=None, requester=None,
+ check_for_existing=True):
"""See `IPOTemplate`."""
- # see if a valid one exists.
- existingpo = self.getPOFileByLang(language_code, variant)
- assert existingpo is None, (
- 'There is already a valid IPOFile (%s)' % existingpo.title)
+ if check_for_existing:
+ # see if a valid one exists.
+ existingpo = self.getPOFileByLang(language.code, variant)
+ assert existingpo is None, (
+ 'There is already a valid IPOFile (%s)' % existingpo.title)
- language = self._lookupLanguage(language_code)
return DummyPOFile(self, language, variant=variant, owner=requester)
def createPOTMsgSetFromMsgIDs(self, msgid_singular, msgid_plural=None,
@@ -1564,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`."""
@@ -1659,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 12:27:52 +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 12:27:52 +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_potemplate.py'
--- lib/lp/translations/tests/test_potemplate.py 2010-07-21 14:02:28 +0000
+++ lib/lp/translations/tests/test_potemplate.py 2010-07-23 12:27:52 +0000
@@ -59,6 +59,31 @@
"missing directory and language code. "
"(Expected: '%s' Got: '%s')" % (expected, result))
+ def test_getDummyPOFile_no_existing_pofile(self):
+ # Test basic behaviour of getDummyPOFile.
+ language = self.factory.makeLanguage('sr@test')
+ dummy = self.potemplate.getDummyPOFile(language)
+ self.assertEquals(DummyPOFile, type(dummy))
+
+ def test_getDummyPOFile_with_existing_pofile(self):
+ # Test that getDummyPOFile fails when trying to get a DummyPOFile
+ # where a POFile already exists for that language.
+ language = self.factory.makeLanguage('sr@test')
+ pofile = self.potemplate.newPOFile(language.code)
+ self.assertRaises(
+ AssertionError, self.potemplate.getDummyPOFile, language)
+
+ def test_getDummyPOFile_with_existing_pofile_no_check(self):
+ # Test that getDummyPOFile succeeds when trying to get a DummyPOFile
+ # where a POFile already exists for that language when
+ # check_for_existing=False is passed in.
+ language = self.factory.makeLanguage('sr@test')
+ pofile = self.potemplate.newPOFile(language.code)
+ # This is just "assertNotRaises".
+ dummy = self.potemplate.getDummyPOFile(language,
+ check_for_existing=False)
+ self.assertEquals(DummyPOFile, type(dummy))
+
def test_getTranslationCredits(self):
# getTranslationCredits returns only translation credits.
self.factory.makePOTMsgSet(self.potemplate, sequence=1)
=== 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 12:27:52 +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 12:27:52 +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 12:27:52 +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