← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~danilo/launchpad/migrate-current-flags into lp:launchpad/devel

 

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

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  #677600 Migration script for upstream sharing for upstream projects
  https://bugs.launchpad.net/bugs/677600


= Bug 677600 =

Provide a script to set is_imported flag for all TranslationMessages on
upstream projects in Launchpad where is_current is set.  This is a
script to aid with transition to the new semantics for the data model
that we are doing in our integration branch
(lp:~launchpad/launchpad/recife).

== Implementation details ==

We are using a traditional tried-and-true migration framework through
DBLoopTuner.  It warrants that we won't overload the DB replication.

There are no tests for the full script run simply because this is a
script that's going to be used only for the migration (basically twice:
once before the rollout, once after it).

== Tests ==

bin/test -cvvt getProductsWithTemplates -t getCurrentNonimportedTranslations -t updateTranslationMessages

== Demo and Q/A ==

This will have to be Q/Ad on staging.  Local runs are simple and
"update" two TranslationMessages total.  Just do

  scripts/rosetta/migrate_current_flag.py

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/translations/scripts/tests/test_migrate_current_flag.py
  lib/lp/translations/scripts/migrate_current_flag.py
  scripts/rosetta/migrate_current_flag.py

./scripts/rosetta/migrate_current_flag.py
       8: '_pythonpath' imported but unused
-- 
https://code.launchpad.net/~danilo/launchpad/migrate-current-flags/+merge/41364
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~danilo/launchpad/migrate-current-flags into lp:launchpad/devel.
=== added file 'lib/lp/translations/scripts/migrate_current_flag.py'
--- lib/lp/translations/scripts/migrate_current_flag.py	1970-01-01 00:00:00 +0000
+++ lib/lp/translations/scripts/migrate_current_flag.py	2010-11-19 20:11:21 +0000
@@ -0,0 +1,145 @@
+# Copyright 2010 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Set 'is_imported' flag from 'is_current' for upstream projects."""
+
+__metaclass__ = type
+__all__ = ['MigrateCurrentFlagProcess']
+
+import logging
+
+from zope.component import getUtility
+from zope.interface import implements
+
+from storm.expr import Count, Select
+
+from canonical.launchpad.interfaces.looptuner import ITunableLoop
+from canonical.launchpad.utilities.looptuner import DBLoopTuner
+from canonical.launchpad.webapp.interfaces import (
+    IStoreSelector,
+    MAIN_STORE,
+    MASTER_FLAVOR,
+    )
+from lp.registry.model.product import Product
+from lp.registry.model.productseries import ProductSeries
+from lp.translations.model.potemplate import POTemplate
+from lp.translations.model.translationmessage import TranslationMessage
+from lp.translations.model.translationtemplateitem import (
+    TranslationTemplateItem,
+    )
+
+
+class TranslationMessageImportedFlagUpdater:
+    implements(ITunableLoop)
+    """Populates is_imported flag from is_current flag on translations."""
+
+    def __init__(self, transaction, logger, tm_ids):
+        self.transaction = transaction
+        self.logger = logger
+        self.start_at = 0
+
+        self.tm_ids = list(tm_ids)
+        self.total = len(self.tm_ids)
+        self.logger.info(
+            "Fixing up a total of %d TranslationMessages." % (self.total))
+        self.store = getUtility(IStoreSelector).get(MAIN_STORE, MASTER_FLAVOR)
+
+    def isDone(self):
+        """See `ITunableLoop`."""
+        # When the main loop hits the end of the list of objects,
+        # it sets start_at to None.
+        return self.start_at is None
+
+    def getNextBatch(self, chunk_size):
+        """Return a batch of objects to work with."""
+        end_at = self.start_at + int(chunk_size)
+        self.logger.debug(
+            "Getting translations[%d:%d]..." % (self.start_at, end_at))
+        return self.tm_ids[self.start_at: end_at]
+
+    def _updateTranslationMessages(self, tm_ids):
+        # Unset imported messages that might be in the way.
+        previous_imported = self.store.find(
+            TranslationMessage,
+            TranslationMessage.is_imported == True,
+            TranslationMessage.potmsgsetID.is_in(
+                Select(TranslationMessage.potmsgsetID,
+                       TranslationMessage.id.is_in(tm_ids))))
+        previous_imported.set(is_imported=False)
+        translations = self.store.find(
+            TranslationMessage,
+            TranslationMessage.id.is_in(tm_ids))
+        translations.set(is_imported=True)
+
+    def __call__(self, chunk_size):
+        """See `ITunableLoop`.
+
+        Retrieve a batch of TranslationMessages in ascending id order,
+        and set is_imported flag to True on all of them.
+        """
+        tm_ids = self.getNextBatch(chunk_size)
+
+        if len(tm_ids) == 0:
+            self.start_at = None
+        else:
+            self._updateTranslationMessages(tm_ids)
+            self.transaction.commit()
+            self.transaction.begin()
+
+            self.start_at += len(tm_ids)
+            self.logger.info("Processed %d/%d TranslationMessages." % (
+                self.start_at, self.total))
+
+
+class MigrateCurrentFlagProcess:
+    """Mark all translations as is_imported if they are is_current.
+
+    Processes only translations for upstream projects, since Ubuntu
+    source packages need no migration.
+    """
+
+    def __init__(self, transaction, logger=None):
+        self.transaction = transaction
+        self.logger = logger
+        if logger is None:
+            self.logger = logging.getLogger("migrate-current-flag")
+        self.store = getUtility(IStoreSelector).get(MAIN_STORE, MASTER_FLAVOR)
+
+    def getProductsWithTemplates(self):
+        """Get Product.ids for projects with any translations templates."""
+        return self.store.find(
+            Product,
+            POTemplate.productseriesID == ProductSeries.id,
+            ProductSeries.productID == Product.id,
+            ).group_by(Product).having(Count(POTemplate.id) > 0)
+
+    def getCurrentNonimportedTranslations(self, product):
+        """Get TranslationMessage.ids that need migration for a `product`."""
+        return self.store.find(
+            TranslationMessage.id,
+            TranslationMessage.is_current == True,
+            TranslationMessage.is_imported == False,
+            (TranslationMessage.potmsgsetID ==
+             TranslationTemplateItem.potmsgsetID),
+            TranslationTemplateItem.potemplateID == POTemplate.id,
+            POTemplate.productseriesID == ProductSeries.id,
+            ProductSeries.productID == product.id).config(distinct=True)
+
+    def run(self):
+        products_with_templates = list(self.getProductsWithTemplates())
+        total_products = len(products_with_templates)
+        if total_products == 0:
+            self.logger.info("Nothing to do.")
+        current_product = 0
+        for product in products_with_templates:
+            current_product += 1
+            self.logger.info(
+                "Migrating %s translations (%d of %d)..." % (
+                    product.name, current_product, total_products))
+
+            tm_ids = self.getCurrentNonimportedTranslations(product)
+            tm_loop = TranslationMessageImportedFlagUpdater(
+                self.transaction, self.logger, tm_ids)
+            DBLoopTuner(tm_loop, 5, minimum_chunk_size=100).run()
+
+        self.logger.info("Done.")

=== added file 'lib/lp/translations/scripts/tests/test_migrate_current_flag.py'
--- lib/lp/translations/scripts/tests/test_migrate_current_flag.py	1970-01-01 00:00:00 +0000
+++ lib/lp/translations/scripts/tests/test_migrate_current_flag.py	2010-11-19 20:11:21 +0000
@@ -0,0 +1,145 @@
+# Copyright 2010 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+__metaclass__ = type
+
+import logging
+
+from canonical.testing.layers import LaunchpadZopelessLayer
+from lp.testing import TestCaseWithFactory
+from lp.translations.scripts.migrate_current_flag import (
+    MigrateCurrentFlagProcess,
+    TranslationMessageImportedFlagUpdater,
+    )
+
+
+class TestMigrateCurrentFlag(TestCaseWithFactory):
+    """Test current-flag migration script."""
+    layer = LaunchpadZopelessLayer
+
+    def setUp(self):
+        # This test needs the privileges of rosettaadmin (to update
+        # TranslationMessages) but it also needs to set up test conditions
+        # which requires other privileges.
+        self.layer.switchDbUser('postgres')
+        super(TestMigrateCurrentFlag, self).setUp(user='mark@xxxxxxxxxxx')
+        self.migrate_process = MigrateCurrentFlagProcess(self.layer.txn)
+
+    def test_getProductsWithTemplates_sampledata(self):
+        # Sample data already has 3 products with templates.
+        sampledata_products = list(
+            self.migrate_process.getProductsWithTemplates())
+        self.assertEquals(3, len(sampledata_products))
+
+    def test_getProductsWithTemplates_noop(self):
+        # Adding a product with no templates doesn't change anything.
+        sampledata_products = list(
+            self.migrate_process.getProductsWithTemplates())
+        self.factory.makeProduct()
+        products = self.migrate_process.getProductsWithTemplates()
+        self.assertContentEqual(sampledata_products, list(products))
+
+    def test_getProductsWithTemplates_new_template(self):
+        # A new product with a template is included.
+        sampledata_products = list(
+            self.migrate_process.getProductsWithTemplates())
+        product = self.factory.makeProduct()
+        self.factory.makePOTemplate(productseries=product.development_focus)
+        products = self.migrate_process.getProductsWithTemplates()
+        self.assertContentEqual(
+            sampledata_products + [product], list(products))
+
+    def test_getCurrentNonimportedTranslations_empty(self):
+        # For a product with no translations no messages are returned.
+        potemplate = self.factory.makePOTemplate()
+        results = list(
+            self.migrate_process.getCurrentNonimportedTranslations(
+                potemplate.productseries.product))
+        self.assertContentEqual([], results)
+
+    def test_getCurrentNonimportedTranslations_noncurrent(self):
+        # For a product with non-current translations no messages
+        # are returned.
+        potemplate = self.factory.makePOTemplate()
+        potmsgset = self.factory.makePOTMsgSet(
+            potemplate=potemplate,
+            sequence=1)
+        pofile = self.factory.makePOFile(potemplate=potemplate)
+        translation = self.factory.makeTranslationMessage(
+            pofile=pofile, potmsgset=potmsgset, suggestion=True)
+        results = list(
+            self.migrate_process.getCurrentNonimportedTranslations(
+                potemplate.productseries.product))
+        self.assertContentEqual([], results)
+
+    def test_getCurrentNonimportedTranslations_current_imported(self):
+        # For a product with current, imported translations no messages
+        # are returned.
+        potemplate = self.factory.makePOTemplate()
+        potmsgset = self.factory.makePOTMsgSet(
+            potemplate=potemplate,
+            sequence=1)
+        pofile = self.factory.makePOFile(potemplate=potemplate)
+        translation = self.factory.makeTranslationMessage(
+            pofile=pofile, potmsgset=potmsgset, is_imported=True)
+        results = list(
+            self.migrate_process.getCurrentNonimportedTranslations(
+                potemplate.productseries.product))
+        self.assertContentEqual([], results)
+
+    def test_getCurrentNonimportedTranslations_current_nonimported(self):
+        # For a product with current, non-imported translations,
+        # that translation is returned.
+        potemplate = self.factory.makePOTemplate()
+        potmsgset = self.factory.makePOTMsgSet(
+            potemplate=potemplate,
+            sequence=1)
+        pofile = self.factory.makePOFile(potemplate=potemplate)
+        translation = self.factory.makeTranslationMessage(
+            pofile=pofile, potmsgset=potmsgset, is_imported=False)
+        results = list(
+            self.migrate_process.getCurrentNonimportedTranslations(
+                potemplate.productseries.product))
+        self.assertContentEqual([translation.id], results)
+
+
+class TestUpdaterLoop(TestCaseWithFactory):
+    """Test updater-loop core functionality."""
+    layer = LaunchpadZopelessLayer
+
+    def setUp(self):
+        # This test needs the privileges of rosettaadmin (to update
+        # TranslationMessages) but it also needs to set up test conditions
+        # which requires other privileges.
+        self.layer.switchDbUser('postgres')
+        super(TestUpdaterLoop, self).setUp(user='mark@xxxxxxxxxxx')
+        self.logger = logging.getLogger("migrate-current-flag")
+        self.migrate_loop = TranslationMessageImportedFlagUpdater(
+            self.layer.txn, self.logger, [])
+
+    def test_updateTranslationMessages_base(self):
+        # Passing in a TranslationMessage.id sets is_imported flag
+        # on that message even if it was not set before.
+        translation = self.factory.makeTranslationMessage()
+        self.assertFalse(translation.is_imported)
+
+        self.migrate_loop._updateTranslationMessages([translation.id])
+        self.assertTrue(translation.is_imported)
+
+    def test_updateTranslationMessages_unsetting_imported(self):
+        # If there was a previous imported message, it is unset
+        # first.
+        pofile = self.factory.makePOFile()
+        imported = self.factory.makeTranslationMessage(
+            pofile=pofile, is_imported=True)
+        translation = self.factory.makeTranslationMessage(
+            pofile=pofile, potmsgset=imported.potmsgset, is_imported=False)
+        self.assertTrue(imported.is_imported)
+        self.assertFalse(imported.is_current)
+        self.assertFalse(translation.is_imported)
+        self.assertTrue(translation.is_current)
+
+        self.migrate_loop._updateTranslationMessages([translation.id])
+        self.assertFalse(imported.is_imported)
+        self.assertTrue(translation.is_imported)
+        self.assertTrue(translation.is_current)

=== added file 'scripts/rosetta/migrate_current_flag.py'
--- scripts/rosetta/migrate_current_flag.py	1970-01-01 00:00:00 +0000
+++ scripts/rosetta/migrate_current_flag.py	2010-11-19 20:11:21 +0000
@@ -0,0 +1,30 @@
+#!/usr/bin/python -S
+#
+# Copyright 2010 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Migrate current flag to imported flag on project translations."""
+
+import _pythonpath
+
+from lp.services.scripts.base import LaunchpadScript
+from lp.translations.scripts.migrate_current_flag import (
+    MigrateCurrentFlagProcess)
+
+
+class MigrateTranslationFlags(LaunchpadScript):
+    """Go through all POFiles and TranslationMessages and get rid of variants.
+
+    Replaces use of `variant` field with a new language with the code
+    corresponding to the 'previous language'@'variant'.
+    """
+
+    def main(self):
+        fixer = MigrateCurrentFlagProcess(self.txn, self.logger)
+        fixer.run()
+
+
+if __name__ == '__main__':
+    script = MigrateTranslationFlags(
+        name="migratecurrentflag", dbuser='rosettaadmin')
+    script.lock_and_run()


Follow ups