← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~abentley/launchpad/share-existing-packagings into lp:launchpad

 

Aaron Bentley has proposed merging lp:~abentley/launchpad/share-existing-packagings into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~abentley/launchpad/share-existing-packagings/+merge/49634

= Summary =
Fix existing un-merged product/package translations

== Proposed fix ==
Implement a script to do this, using the same functionality as
TranslationMergeJobs use

== Pre-implementation notes ==
Discussed with deryck.

== Implementation details ==
The query to find suitable Packagings uses a subselect because the Storm
ClassAlias functionality appears to be broken.  Specifically, it does not do
"FROM POTemplate AS PackageTemplate", just "FROM POTemplate", which means that
it looks for templates associated with both a package and a product, which
don't exist.

I would have converted makeTranslationMergeJob to a function, but I have
another pending branch which does this.

== Tests ==
bin/test -vt TestFindMergablePackagings -t TestMergeExistingPackagings

== Demo and Q/A ==
Run the script on staging, check the output and results.


= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/translations/tests/test_translationmerger.py
  lib/lp/translations/translationmerger.py
  lib/lp/registry/model/packaging.py
  scripts/rosetta/merge-existing-packagings.py
  lib/lp/translations/model/translationmergejob.py
  lib/lp/testing/factory.py
  lib/lp/translations/tests/test_translationmergejob.py
  lib/lp/translations/scripts/tests/test_merge_existing_packagings.py

./scripts/rosetta/merge-existing-packagings.py
       8: '_pythonpath' imported but unused
-- 
https://code.launchpad.net/~abentley/launchpad/share-existing-packagings/+merge/49634
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~abentley/launchpad/share-existing-packagings into lp:launchpad.
=== modified file 'lib/lp/registry/model/packaging.py'
--- lib/lp/registry/model/packaging.py	2010-08-20 20:31:18 +0000
+++ lib/lp/registry/model/packaging.py	2011-02-14 14:46:20 +0000
@@ -71,11 +71,11 @@
             raise AssertionError(
                 "A packaging entry for %s in %s already exists." %
                 (sourcepackagename.name, distroseries.name))
-        Packaging(productseries=productseries,
-                  sourcepackagename=sourcepackagename,
-                  distroseries=distroseries,
-                  packaging=packaging,
-                  owner=owner)
+        return Packaging(productseries=productseries,
+                         sourcepackagename=sourcepackagename,
+                         distroseries=distroseries,
+                         packaging=packaging,
+                         owner=owner)
 
     def deletePackaging(self, productseries, sourcepackagename, distroseries):
         """See `IPackaging`."""

=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py	2011-02-04 17:58:42 +0000
+++ lib/lp/testing/factory.py	2011-02-14 14:46:20 +0000
@@ -2643,8 +2643,15 @@
     def makePOTemplate(self, productseries=None, distroseries=None,
                        sourcepackagename=None, owner=None, name=None,
                        translation_domain=None, path=None,
-                       copy_pofiles=True, side=None):
+                       copy_pofiles=True, side=None, sourcepackage=None):
         """Make a new translation template."""
+        if sourcepackage is not None:
+            assert distroseries is None, (
+                'Cannot specify sourcepackage and distroseries')
+            distroseries = sourcepackage.distroseries
+            assert sourcepackagename is None, (
+                'Cannot specify sourcepackage and sourcepackagename')
+            sourcepackagename = sourcepackage.sourcepackagename
         if productseries is None and distroseries is None:
             if side != TranslationSide.UBUNTU:
                 # No context for this template; set up a productseries.

=== modified file 'lib/lp/translations/model/translationmergejob.py'
--- lib/lp/translations/model/translationmergejob.py	2011-02-09 15:39:26 +0000
+++ lib/lp/translations/model/translationmergejob.py	2011-02-14 14:46:20 +0000
@@ -10,7 +10,6 @@
 
 from lp.services.job.interfaces.job import IRunnableJob
 from lp.services.job.runner import BaseRunnableJob
-from lp.translations.model.potemplate import POTemplate, POTemplateSubset
 from lp.translations.translationmerger import (
     TransactionManager,
     TranslationMerger,
@@ -39,16 +38,6 @@
 
     def run(self):
         """See `IRunnableJob`."""
-        template_map = dict()
         tm = TransactionManager(None, False)
-        all_templates = list(POTemplateSubset(
-            sourcepackagename=self.sourcepackagename,
-            distroseries=self.distroseries))
-        all_templates.extend(POTemplateSubset(
-            productseries=self.productseries))
-        for template in all_templates:
-            template_map.setdefault(template.name, []).append(template)
-        for name, templates in template_map.iteritems():
-            templates.sort(key=POTemplate.sharingKey, reverse=True)
-            merger = TranslationMerger(templates, tm)
-            merger.mergePOTMsgSets()
+        TranslationMerger.mergePackagingTemplates(
+            self.productseries, self.sourcepackagename, self.distroseries, tm)

=== added file 'lib/lp/translations/scripts/tests/test_merge_existing_packagings.py'
--- lib/lp/translations/scripts/tests/test_merge_existing_packagings.py	1970-01-01 00:00:00 +0000
+++ lib/lp/translations/scripts/tests/test_merge_existing_packagings.py	2011-02-14 14:46:20 +0000
@@ -0,0 +1,44 @@
+# Copyright 2011 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Test the merge_translations script."""
+
+
+import transaction
+
+from canonical.launchpad.scripts.tests import run_script
+from canonical.testing.layers import ZopelessAppServerLayer
+from lp.translations.translationmerger import TranslationMerger
+from lp.testing import TestCaseWithFactory
+
+
+class TestMergeExistingPackagings(TestCaseWithFactory):
+
+    layer = ZopelessAppServerLayer
+
+    def test_merge_translations(self):
+        """Running the script performs a translation merge."""
+        from lp.translations.tests.test_translationmergejob import (
+            TestTranslationMergeJob,
+            )
+        # Import here to avoid autodetection by test runner.
+        for packaging in set(TranslationMerger.findMergeablePackagings()):
+            packaging.destroySelf()
+        job = TestTranslationMergeJob.makeTranslationMergeJob(self.factory)
+        packaging = self.factory.makePackagingLink(job.productseries,
+                job.sourcepackagename, job.distroseries)
+        self.assertEqual(2, TestTranslationMergeJob.countTranslations(job))
+        transaction.commit()
+        retcode, stdout, stderr = run_script(
+            'scripts/rosetta/merge-existing-packagings.py', [],
+            expect_returncode=0)
+        merge_message = 'INFO    Merging %s/%s and %s/%s.\n' % (
+            packaging.productseries.product.name,
+            packaging.productseries.name,
+            packaging.sourcepackagename.name, packaging.distroseries.name)
+        self.assertEqual(
+            merge_message +
+            'INFO    Deleted POTMsgSets: 1.  TranslationMessages: 1.\n',
+            stderr)
+        self.assertEqual('', stdout)
+        self.assertEqual(1, TestTranslationMergeJob.countTranslations(job))

=== modified file 'lib/lp/translations/tests/test_translationmergejob.py'
--- lib/lp/translations/tests/test_translationmergejob.py	2011-02-08 17:11:11 +0000
+++ lib/lp/translations/tests/test_translationmergejob.py	2011-02-14 14:46:20 +0000
@@ -22,21 +22,22 @@
 
     layer = LaunchpadZopelessLayer
 
-    def makeTranslationMergeJob(self):
-        singular = self.factory.getUniqueString()
-        upstream_pofile = self.factory.makePOFile(
+    @staticmethod
+    def makeTranslationMergeJob(factory):
+        singular = factory.getUniqueString()
+        upstream_pofile = factory.makePOFile(
             side=TranslationSide.UPSTREAM)
-        upstream_potmsgset = self.factory.makePOTMsgSet(
+        upstream_potmsgset = factory.makePOTMsgSet(
             upstream_pofile.potemplate, singular, sequence=1)
-        upstream = self.factory.makeCurrentTranslationMessage(
+        upstream = factory.makeCurrentTranslationMessage(
             pofile=upstream_pofile, potmsgset=upstream_potmsgset)
-        ubuntu_potemplate = self.factory.makePOTemplate(
+        ubuntu_potemplate = factory.makePOTemplate(
             side=TranslationSide.UBUNTU, name=upstream_pofile.potemplate.name)
-        ubuntu_pofile = self.factory.makePOFile(
+        ubuntu_pofile = factory.makePOFile(
             potemplate=ubuntu_potemplate, language=upstream_pofile.language)
-        ubuntu_potmsgset = self.factory.makePOTMsgSet(
+        ubuntu_potmsgset = factory.makePOTMsgSet(
             ubuntu_pofile.potemplate, singular, sequence=1)
-        ubuntu = self.factory.makeCurrentTranslationMessage(
+        ubuntu = factory.makeCurrentTranslationMessage(
             pofile=ubuntu_pofile, potmsgset=ubuntu_potmsgset,
             translations=upstream.translations)
         productseries = upstream_pofile.potemplate.productseries
@@ -47,7 +48,7 @@
 
     def test_interface(self):
         """TranslationMergeJob must implement IRunnableJob."""
-        job = self.makeTranslationMergeJob()
+        job = self.makeTranslationMergeJob(self.factory)
         verifyObject(IRunnableJob, job)
 
     @staticmethod
@@ -81,7 +82,7 @@
 
     def test_run_merges_msgset(self):
         """Run should merge msgsets."""
-        job = self.makeTranslationMergeJob()
+        job = self.makeTranslationMergeJob(self.factory)
         self.becomeDbUser('rosettaadmin')
         product_msg = self.getMsgSets(productseries=job.productseries)
         package_msg = self.getMsgSets(
@@ -97,7 +98,7 @@
 
     def test_run_merges_translations(self):
         """Run should merge translations."""
-        job = self.makeTranslationMergeJob()
+        job = self.makeTranslationMergeJob(self.factory)
         self.becomeDbUser('rosettaadmin')
         self.assertEqual(2, self.countTranslations(job))
         job.run()

=== modified file 'lib/lp/translations/tests/test_translationmerger.py'
--- lib/lp/translations/tests/test_translationmerger.py	2011-02-04 15:44:45 +0000
+++ lib/lp/translations/tests/test_translationmerger.py	2011-02-14 14:46:20 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009 Canonical Ltd.  This software is licensed under the
+# Copyright 2009, 2011 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 __metaclass__ = type
@@ -807,5 +807,70 @@
             POTranslation._table, recorder.statements)
 
 
+class TestFindMergablePackagings(TestCaseWithFactory):
+    """Test TranslationMerger.findMergeablePackagings."""
+
+    layer = LaunchpadZopelessLayer
+
+    def setUp(self):
+        """Remove sample data to simplify tests."""
+        super(TestFindMergablePackagings, self).setUp()
+        for packaging in set(TranslationMerger.findMergeablePackagings()):
+            packaging.destroySelf()
+
+    def makePackagingLink(self, non_ubuntu=False):
+        if non_ubuntu:
+            distroseries = self.factory.makeDistroSeries()
+        else:
+            distroseries = self.factory.makeUbuntuDistroSeries()
+        return self.factory.makePackagingLink(distroseries=distroseries)
+
+    def test_no_templates(self):
+        """A Packaging with no templates is ignored."""
+        packaging = self.makePackagingLink()
+        self.assertContentEqual(
+            [], TranslationMerger.findMergeablePackagings())
+
+    def test_no_product_template(self):
+        """A Packaging with no product templates is ignored."""
+        packaging = self.makePackagingLink()
+        self.factory.makePOTemplate(sourcepackage=packaging.sourcepackage)
+        self.assertContentEqual(
+            [], TranslationMerger.findMergeablePackagings())
+
+    def test_no_package_template(self):
+        """A Packaging with no sourcepackage templates is ignored."""
+        packaging = self.makePackagingLink()
+        self.factory.makePOTemplate(productseries=packaging.productseries)
+        self.assertContentEqual(
+            [], TranslationMerger.findMergeablePackagings())
+
+    def test_both_templates(self):
+        """A Packaging with product and package templates is included."""
+        packaging = self.makePackagingLink()
+        self.factory.makePOTemplate(productseries=packaging.productseries)
+        self.factory.makePOTemplate(sourcepackage=packaging.sourcepackage)
+        self.assertContentEqual(
+            [packaging], TranslationMerger.findMergeablePackagings())
+
+    def test_multiple_templates(self):
+        """A Packaging with multiple templates appears only once."""
+        packaging = self.makePackagingLink()
+        self.factory.makePOTemplate(productseries=packaging.productseries)
+        self.factory.makePOTemplate(productseries=packaging.productseries)
+        self.factory.makePOTemplate(sourcepackage=packaging.sourcepackage)
+        self.factory.makePOTemplate(sourcepackage=packaging.sourcepackage)
+        self.assertContentEqual(
+            [packaging], TranslationMerger.findMergeablePackagings())
+
+    def test_non_ubuntu(self):
+        """A Packaging not for Ubuntu is ignored."""
+        packaging = self.makePackagingLink(non_ubuntu=True)
+        self.factory.makePOTemplate(productseries=packaging.productseries)
+        self.factory.makePOTemplate(sourcepackage=packaging.sourcepackage)
+        self.assertContentEqual(
+            [], TranslationMerger.findMergeablePackagings())
+
+
 def test_suite():
     return TestLoader().loadTestsFromName(__name__)

=== modified file 'lib/lp/translations/translationmerger.py'
--- lib/lp/translations/translationmerger.py	2011-02-09 16:06:28 +0000
+++ lib/lp/translations/translationmerger.py	2011-02-14 14:46:20 +0000
@@ -11,17 +11,26 @@
 
 from operator import methodcaller
 
-from storm.locals import Store
+from storm.locals import (
+    Select,
+    Store,
+    )
 from zope.component import getUtility
 from zope.security.proxy import removeSecurityProxy
 
+from canonical.launchpad.interfaces.lpstorm import (
+    IStore,
+    )
 from canonical.launchpad.scripts.logger import (
     DEBUG2,
     log,
     )
+from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
 from canonical.launchpad.utilities.orderingcheck import OrderingCheck
 from lp.registry.interfaces.product import IProductSet
 from lp.registry.interfaces.sourcepackagename import ISourcePackageNameSet
+from lp.registry.model.distroseries import DistroSeries
+from lp.registry.model.packaging import Packaging
 from lp.services.scripts.base import (
     LaunchpadScript,
     LaunchpadScriptFailure,
@@ -29,8 +38,10 @@
 from lp.translations.interfaces.potemplate import IPOTemplateSet
 from lp.translations.interfaces.side import TranslationSide
 from lp.translations.interfaces.translations import TranslationConstants
+from lp.translations.model.potemplate import POTemplateSubset
 from lp.translations.model.potmsgset import POTMsgSet
 from lp.translations.model.translationmessage import TranslationMessage
+from lp.translations.model.potemplate import POTemplate
 
 
 def get_potmsgset_key(potmsgset):
@@ -342,6 +353,51 @@
 class TranslationMerger:
     """Merge translations across a set of potemplates."""
 
+    @staticmethod
+    def findMergeablePackagings():
+        """Find packagings where both product and package have templates."""
+        store = IStore(Packaging)
+        upstream_translated = Select(
+            Packaging.id,
+            Packaging.productseries == POTemplate.productseriesID,
+            )
+        ubuntu = getUtility(ILaunchpadCelebrities).ubuntu
+        result = store.find(
+            Packaging, Packaging.id.is_in(upstream_translated),
+            Packaging.distroseries == POTemplate.distroseriesID,
+            Packaging.distroseries == DistroSeries.id,
+            DistroSeries.distribution == ubuntu.id,
+            Packaging.sourcepackagename == POTemplate.sourcepackagenameID,
+            )
+        # This should be as simple as the following, but apparently Storm
+        # ClassAlias doesn't work properly:
+        # PackageTemplate = ClassAlias(POTemplate)
+        # result = store.find(
+        #     Packaging,
+        #     Packaging.productseries == POTemplate.productseries,
+        #     Packaging.distroseries == PackageTemplate.distroseries,
+        #     Packaging.sourcepackagename ==
+        #         PackageTemplate.sourcepackagename,
+        #     )
+        result.config(distinct=True)
+        return result
+
+    @classmethod
+    def mergePackagingTemplates(cls, productseries, sourcepackagename,
+                                distroseries, tm):
+        template_map = dict()
+        all_templates = list(POTemplateSubset(
+            sourcepackagename=sourcepackagename,
+            distroseries=distroseries))
+        all_templates.extend(POTemplateSubset(
+            productseries=productseries))
+        for template in all_templates:
+            template_map.setdefault(template.name, []).append(template)
+        for name, templates in template_map.iteritems():
+            templates.sort(key=POTemplate.sharingKey, reverse=True)
+            merger = cls(templates, tm)
+            merger.mergePOTMsgSets()
+
     def __init__(self, potemplates, tm):
         """Constructor.
 
@@ -713,3 +769,20 @@
             bequeathe_flags(message, twin)
 
         return True
+
+
+class MergeExistingPackagings(LaunchpadScript):
+    """Script to perform translation on existing packagings."""
+
+    def main(self):
+        tm = TransactionManager(self.txn, False)
+        for packaging in TranslationMerger.findMergeablePackagings():
+            log.info('Merging %s/%s and %s/%s.' % (
+                packaging.productseries.product.name,
+                packaging.productseries.name,
+                packaging.sourcepackagename.name,
+                packaging.distroseries.name))
+            TranslationMerger.mergePackagingTemplates(
+                packaging.productseries, packaging.sourcepackagename,
+                packaging.distroseries, tm)
+            tm.endTransaction(False)

=== added file 'scripts/rosetta/merge-existing-packagings.py'
--- scripts/rosetta/merge-existing-packagings.py	1970-01-01 00:00:00 +0000
+++ scripts/rosetta/merge-existing-packagings.py	2011-02-14 14:46:20 +0000
@@ -0,0 +1,18 @@
+#!/usr/bin/python -S
+#
+# Copyright 2011 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+__metaclass__ = type
+
+import _pythonpath
+
+from lp.translations.translationmerger import (
+    MergeExistingPackagings)
+
+
+if __name__ == '__main__':
+    script = MergeExistingPackagings(
+        'canonical.launchpad.scripts.message-sharing-merge',
+        dbuser='rosettaadmin')
+    script.run()