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

Proposed by Данило Шеган on 2010-11-19
Status: Merged
Approved by: Aaron Bentley on 2010-11-25
Approved revision: no longer in the source branch.
Merged at revision: 11982
Proposed branch: lp:~danilo/launchpad/migrate-current-flags
Merge into: lp:launchpad
Diff against target: 388 lines (+374/-0)
3 files modified
lib/lp/translations/scripts/migrate_current_flag.py (+161/-0)
lib/lp/translations/scripts/tests/test_migrate_current_flag.py (+183/-0)
scripts/rosetta/migrate_current_flag.py (+30/-0)
To merge this branch: bzr merge lp:~danilo/launchpad/migrate-current-flags
Reviewer Review Type Date Requested Status
Aaron Bentley (community) 2010-11-19 Approve on 2010-11-25
Review via email: mp+41364@code.launchpad.net

Commit Message

Provide a script to set is_imported flag based on is_current flag for upstream projects.

Description of the Change

= 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

To post a comment you must log in.
Aaron Bentley (abentley) wrote :

There appear to be multiple cases where this could do the wrong thing:

<jtv> The code seems to assume that there can be only one is_imported TM per POTMsgSet.
 In which case, it's perfectly valid to say "I'm doing each TM once, and for each, I'll clear any previous is_imported flags first."
 But what of another TM for the same POTMsgSet and a different language, that's already been processed?
 It's had its flag set in a previous iteration, but now gets it callously cleared again "to make room" for a TM it doesn't even compete with. AFAICT there's a similar situation with divergence (which is the case where TM.potemplate is non-null).

review: Needs Fixing
Данило Шеган (danilo) wrote :

У пон, 22. 11 2010. у 15:21 +0000, Aaron Bentley пише:
> Review: Needs Fixing
> There appear to be multiple cases where this could do the wrong thing:
>
> <jtv> The code seems to assume that there can be only one is_imported TM per POTMsgSet.
> In which case, it's perfectly valid to say "I'm doing each TM once, and for each, I'll clear any previous is_imported flags first."
> But what of another TM for the same POTMsgSet and a different language, that's already been processed?
> It's had its flag set in a previous iteration, but now gets it callously cleared again "to make room" for a TM it doesn't even compete with. AFAICT there's a similar situation with divergence (which is the case where TM.potemplate is non-null).

Indeed, all well spotted. A few tests for the above and an
implementation that unsets only TMs that really are in the way.

Incremental diff attached.

1=== modified file 'lib/lp/translations/scripts/migrate_current_flag.py'
2--- lib/lp/translations/scripts/migrate_current_flag.py 2010-11-19 18:59:45 +0000
3+++ lib/lp/translations/scripts/migrate_current_flag.py 2010-11-22 23:14:11 +0000
4@@ -11,7 +11,8 @@
5 from zope.component import getUtility
6 from zope.interface import implements
7
8-from storm.expr import Count, Select
9+from storm.info import ClassAlias
10+from storm.expr import And, Count, Or, Select
11
12 from canonical.launchpad.interfaces.looptuner import ITunableLoop
13 from canonical.launchpad.utilities.looptuner import DBLoopTuner
14@@ -59,12 +60,27 @@
15
16 def _updateTranslationMessages(self, tm_ids):
17 # Unset imported messages that might be in the way.
18+ PreviousImported = ClassAlias(
19+ TranslationMessage, 'PreviousImported')
20+ CurrentTranslation = ClassAlias(
21+ TranslationMessage, 'CurrentTranslation')
22+ previous_imported_select = Select(
23+ PreviousImported.id,
24+ tables=[PreviousImported, CurrentTranslation],
25+ where=And(
26+ PreviousImported.is_imported == True,
27+ (PreviousImported.potmsgsetID ==
28+ CurrentTranslation.potmsgsetID),
29+ Or(And(PreviousImported.potemplate == None,
30+ CurrentTranslation.potemplate == None),
31+ (PreviousImported.potemplateID ==
32+ CurrentTranslation.potemplateID)),
33+ PreviousImported.languageID == CurrentTranslation.languageID,
34+ CurrentTranslation.id.is_in(tm_ids)))
35+
36 previous_imported = self.store.find(
37 TranslationMessage,
38- TranslationMessage.is_imported == True,
39- TranslationMessage.potmsgsetID.is_in(
40- Select(TranslationMessage.potmsgsetID,
41- TranslationMessage.id.is_in(tm_ids))))
42+ TranslationMessage.id.is_in(previous_imported_select))
43 previous_imported.set(is_imported=False)
44 translations = self.store.find(
45 TranslationMessage,
46
47=== modified file 'lib/lp/translations/scripts/tests/test_migrate_current_flag.py'
48--- lib/lp/translations/scripts/tests/test_migrate_current_flag.py 2010-11-19 18:59:45 +0000
49+++ lib/lp/translations/scripts/tests/test_migrate_current_flag.py 2010-11-22 22:38:39 +0000
50@@ -143,3 +143,41 @@
51 self.assertFalse(imported.is_imported)
52 self.assertTrue(translation.is_imported)
53 self.assertTrue(translation.is_current)
54+
55+ def test_updateTranslationMessages_other_language(self):
56+ # If there was a previous imported message in another language
57+ # it is not unset.
58+ pofile = self.factory.makePOFile()
59+ pofile_other = self.factory.makePOFile(potemplate=pofile.potemplate)
60+ imported = self.factory.makeTranslationMessage(
61+ pofile=pofile_other, is_imported=True)
62+ translation = self.factory.makeTranslationMessage(
63+ pofile=pofile, potmsgset=imported.potmsgset, is_imported=False)
64+ self.assertTrue(imported.is_imported)
65+ self.assertTrue(imported.is_current)
66+ self.assertFalse(translation.is_imported)
67+ self.assertTrue(translation.is_current)
68+
69+ self.migrate_loop._updateTranslationMessages([translation.id])
70+ self.assertTrue(imported.is_imported)
71+ self.assertTrue(imported.is_current)
72+ self.assertTrue(translation.is_imported)
73+ self.assertTrue(translation.is_current)
74+
75+ def test_updateTranslationMessages_diverged(self):
76+ # If there was a previous diverged message, it is not
77+ # touched.
78+ pofile = self.factory.makePOFile()
79+ translation = self.factory.makeTranslationMessage(
80+ pofile=pofile, is_imported=False)
81+ diverged_imported = self.factory.makeTranslationMessage(
82+ pofile=pofile, force_diverged=True, is_imported=True,
83+ potmsgset=translation.potmsgset)
84+ self.assertEquals(pofile.potemplate, diverged_imported.potemplate)
85+ self.assertTrue(diverged_imported.is_imported)
86+ self.assertTrue(diverged_imported.is_current)
87+
88+ self.migrate_loop._updateTranslationMessages([translation.id])
89+ self.assertEquals(pofile.potemplate, diverged_imported.potemplate)
90+ self.assertTrue(diverged_imported.is_imported)
91+ self.assertTrue(diverged_imported.is_current)
92
Aaron Bentley (abentley) wrote :

Jtv was planning to address these issues.

"Данило Шеган" <email address hidden> wrote:

>У пон, 22. 11 2010. у 15:21 +0000, Aaron Bentley пише:
>> Review: Needs Fixing
>> There appear to be multiple cases where this could do the wrong
>thing:
>>
>> <jtv> The code seems to assume that there can be only one is_imported
>TM per POTMsgSet.
>> In which case, it's perfectly valid to say "I'm doing each TM once,
>and for each, I'll clear any previous is_imported flags first."
>> But what of another TM for the same POTMsgSet and a different
>language, that's already been processed?
>> It's had its flag set in a previous iteration, but now gets it
>callously cleared again "to make room" for a TM it doesn't even compete
>with. AFAICT there's a similar situation with divergence (which is the
>case where TM.potemplate is non-null).
>
>Indeed, all well spotted. A few tests for the above and an
>implementation that unsets only TMs that really are in the way.
>
>Incremental diff attached.
>
>
>--
>https://code.launchpad.net/~danilo/launchpad/migrate-current-flags/+merge/41364
>You are reviewing the proposed merge of
>lp:~danilo/launchpad/migrate-current-flags into lp:launchpad/devel.

--
Sent from my phone. Please excuse my brevity.

Jeroen T. Vermeulen (jtv) wrote :

I actually started a branch based on this review, though I saved the important bit for when I was fresh in the morning. (Which is _naturally_ also when I lost power, a hard disk, and a router—why do I even bother with an expensive UPS?)

Anyway, my branch was lp:~jtv/launchpad/migrate-current-flags.

Jeroen T. Vermeulen (jtv) wrote :

Does this mean that Storm translates "X.potemplateID == Y.potemplateID" to "X.potemplate IS NOT DISTINCT FROM Y.potemplate" or something equivalent? I naïvely thought it would translate to "X.potemplate = Y.potemplate."

That's another gotcha with ORMs. Having to worry about where you're getting application-language semantics and where it's SQL semantics. :/

Данило Шеган (danilo) wrote :

У уто, 23. 11 2010. у 07:57 +0000, Jeroen T. Vermeulen пише:
> Does this mean that Storm translates "X.potemplateID == Y.potemplateID"
> to "X.potemplate IS NOT DISTINCT FROM Y.potemplate" or something
> equivalent? I naïvely thought it would translate to "X.potemplate =
> Y.potemplate."

No, it translates it to what you expect. That's why the code is:

                Or(And(PreviousImported.potemplate == None,
                       CurrentTranslation.potemplate == None),
                   (PreviousImported.potemplateID ==
                    CurrentTranslation.potemplateID)),

Existing test ("unsetting_imported") makes sure it works for the
sharing (None) case, and a new diverged-test makes sure it works for the
mixed diverged-shared case.

A test case for diverged-diverged is missing, though I am pretty sure
that works. A test case for shared-diverged is also missing (i.e. shared
existing imported translation, diverged current translation).

> That's another gotcha with ORMs. Having to worry about where you're
> getting application-language semantics and where it's SQL semantics.
> :/

It may come as a surprise, but I still do know a few things about ORMs
(and you may remember that it was me who found out about DISTINCT FROM
in the first place, exactly because we needed it so often :).

Jeroen, also, before I continued on this, I checked if you assigned the
card or the bug to yourself. You didn't so I fixed the branch in order
to help out (and it was very easy for me).

Jeroen T. Vermeulen (jtv) wrote :

OK, if it's still yours, please go ahead and land it. (I don't have workable EC2 right now).

Данило Шеган (danilo) wrote :

It'd be nice if there was a review statement to accompany that :)

Jeroen T. Vermeulen (jtv) wrote :

Ah, of course. We'll see if we can procure that.

Aaron Bentley (abentley) wrote :

jtv's branch has some of the changes that jtv made during my on-call review on IRC. Most of these comments would not apply if you had merged it.

"from storm.expr import And, Count, Or, Select" should be formatted as a multi-line import. It's also better to use storm.locals so you don't need multiple import statements:

from storm.expr import (
    And,
    Count,
    Or,
    Select,
    )

Similarly:

from lp.translations.scripts.migrate_current_flag import (
    MigrateCurrentFlagProcess)

Should be:

from lp.translations.scripts.migrate_current_flag import (
    MigrateCurrentFlagProcess,
)

I also asked for a docstring for _updateTranslationMessages. It's hard to see exactly what it does.

Also, the docstring for MigrateTranslationFlags looks inaccurate (copy & paste).

None of these affect actual functionality, so I won't block landing, but I'd appreciate if you could fix them at some point.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'lib/lp/translations/scripts/migrate_current_flag.py'
2--- lib/lp/translations/scripts/migrate_current_flag.py 1970-01-01 00:00:00 +0000
3+++ lib/lp/translations/scripts/migrate_current_flag.py 2010-11-22 23:19:36 +0000
4@@ -0,0 +1,161 @@
5+# Copyright 2010 Canonical Ltd. This software is licensed under the
6+# GNU Affero General Public License version 3 (see the file LICENSE).
7+
8+"""Set 'is_imported' flag from 'is_current' for upstream projects."""
9+
10+__metaclass__ = type
11+__all__ = ['MigrateCurrentFlagProcess']
12+
13+import logging
14+
15+from zope.component import getUtility
16+from zope.interface import implements
17+
18+from storm.info import ClassAlias
19+from storm.expr import And, Count, Or, Select
20+
21+from canonical.launchpad.interfaces.looptuner import ITunableLoop
22+from canonical.launchpad.utilities.looptuner import DBLoopTuner
23+from canonical.launchpad.webapp.interfaces import (
24+ IStoreSelector,
25+ MAIN_STORE,
26+ MASTER_FLAVOR,
27+ )
28+from lp.registry.model.product import Product
29+from lp.registry.model.productseries import ProductSeries
30+from lp.translations.model.potemplate import POTemplate
31+from lp.translations.model.translationmessage import TranslationMessage
32+from lp.translations.model.translationtemplateitem import (
33+ TranslationTemplateItem,
34+ )
35+
36+
37+class TranslationMessageImportedFlagUpdater:
38+ implements(ITunableLoop)
39+ """Populates is_imported flag from is_current flag on translations."""
40+
41+ def __init__(self, transaction, logger, tm_ids):
42+ self.transaction = transaction
43+ self.logger = logger
44+ self.start_at = 0
45+
46+ self.tm_ids = list(tm_ids)
47+ self.total = len(self.tm_ids)
48+ self.logger.info(
49+ "Fixing up a total of %d TranslationMessages." % (self.total))
50+ self.store = getUtility(IStoreSelector).get(MAIN_STORE, MASTER_FLAVOR)
51+
52+ def isDone(self):
53+ """See `ITunableLoop`."""
54+ # When the main loop hits the end of the list of objects,
55+ # it sets start_at to None.
56+ return self.start_at is None
57+
58+ def getNextBatch(self, chunk_size):
59+ """Return a batch of objects to work with."""
60+ end_at = self.start_at + int(chunk_size)
61+ self.logger.debug(
62+ "Getting translations[%d:%d]..." % (self.start_at, end_at))
63+ return self.tm_ids[self.start_at: end_at]
64+
65+ def _updateTranslationMessages(self, tm_ids):
66+ # Unset imported messages that might be in the way.
67+ PreviousImported = ClassAlias(
68+ TranslationMessage, 'PreviousImported')
69+ CurrentTranslation = ClassAlias(
70+ TranslationMessage, 'CurrentTranslation')
71+ previous_imported_select = Select(
72+ PreviousImported.id,
73+ tables=[PreviousImported, CurrentTranslation],
74+ where=And(
75+ PreviousImported.is_imported == True,
76+ (PreviousImported.potmsgsetID ==
77+ CurrentTranslation.potmsgsetID),
78+ Or(And(PreviousImported.potemplate == None,
79+ CurrentTranslation.potemplate == None),
80+ (PreviousImported.potemplateID ==
81+ CurrentTranslation.potemplateID)),
82+ PreviousImported.languageID == CurrentTranslation.languageID,
83+ CurrentTranslation.id.is_in(tm_ids)))
84+
85+ previous_imported = self.store.find(
86+ TranslationMessage,
87+ TranslationMessage.id.is_in(previous_imported_select))
88+ previous_imported.set(is_imported=False)
89+ translations = self.store.find(
90+ TranslationMessage,
91+ TranslationMessage.id.is_in(tm_ids))
92+ translations.set(is_imported=True)
93+
94+ def __call__(self, chunk_size):
95+ """See `ITunableLoop`.
96+
97+ Retrieve a batch of TranslationMessages in ascending id order,
98+ and set is_imported flag to True on all of them.
99+ """
100+ tm_ids = self.getNextBatch(chunk_size)
101+
102+ if len(tm_ids) == 0:
103+ self.start_at = None
104+ else:
105+ self._updateTranslationMessages(tm_ids)
106+ self.transaction.commit()
107+ self.transaction.begin()
108+
109+ self.start_at += len(tm_ids)
110+ self.logger.info("Processed %d/%d TranslationMessages." % (
111+ self.start_at, self.total))
112+
113+
114+class MigrateCurrentFlagProcess:
115+ """Mark all translations as is_imported if they are is_current.
116+
117+ Processes only translations for upstream projects, since Ubuntu
118+ source packages need no migration.
119+ """
120+
121+ def __init__(self, transaction, logger=None):
122+ self.transaction = transaction
123+ self.logger = logger
124+ if logger is None:
125+ self.logger = logging.getLogger("migrate-current-flag")
126+ self.store = getUtility(IStoreSelector).get(MAIN_STORE, MASTER_FLAVOR)
127+
128+ def getProductsWithTemplates(self):
129+ """Get Product.ids for projects with any translations templates."""
130+ return self.store.find(
131+ Product,
132+ POTemplate.productseriesID == ProductSeries.id,
133+ ProductSeries.productID == Product.id,
134+ ).group_by(Product).having(Count(POTemplate.id) > 0)
135+
136+ def getCurrentNonimportedTranslations(self, product):
137+ """Get TranslationMessage.ids that need migration for a `product`."""
138+ return self.store.find(
139+ TranslationMessage.id,
140+ TranslationMessage.is_current == True,
141+ TranslationMessage.is_imported == False,
142+ (TranslationMessage.potmsgsetID ==
143+ TranslationTemplateItem.potmsgsetID),
144+ TranslationTemplateItem.potemplateID == POTemplate.id,
145+ POTemplate.productseriesID == ProductSeries.id,
146+ ProductSeries.productID == product.id).config(distinct=True)
147+
148+ def run(self):
149+ products_with_templates = list(self.getProductsWithTemplates())
150+ total_products = len(products_with_templates)
151+ if total_products == 0:
152+ self.logger.info("Nothing to do.")
153+ current_product = 0
154+ for product in products_with_templates:
155+ current_product += 1
156+ self.logger.info(
157+ "Migrating %s translations (%d of %d)..." % (
158+ product.name, current_product, total_products))
159+
160+ tm_ids = self.getCurrentNonimportedTranslations(product)
161+ tm_loop = TranslationMessageImportedFlagUpdater(
162+ self.transaction, self.logger, tm_ids)
163+ DBLoopTuner(tm_loop, 5, minimum_chunk_size=100).run()
164+
165+ self.logger.info("Done.")
166
167=== added file 'lib/lp/translations/scripts/tests/test_migrate_current_flag.py'
168--- lib/lp/translations/scripts/tests/test_migrate_current_flag.py 1970-01-01 00:00:00 +0000
169+++ lib/lp/translations/scripts/tests/test_migrate_current_flag.py 2010-11-22 23:19:36 +0000
170@@ -0,0 +1,183 @@
171+# Copyright 2010 Canonical Ltd. This software is licensed under the
172+# GNU Affero General Public License version 3 (see the file LICENSE).
173+
174+__metaclass__ = type
175+
176+import logging
177+
178+from canonical.testing.layers import LaunchpadZopelessLayer
179+from lp.testing import TestCaseWithFactory
180+from lp.translations.scripts.migrate_current_flag import (
181+ MigrateCurrentFlagProcess,
182+ TranslationMessageImportedFlagUpdater,
183+ )
184+
185+
186+class TestMigrateCurrentFlag(TestCaseWithFactory):
187+ """Test current-flag migration script."""
188+ layer = LaunchpadZopelessLayer
189+
190+ def setUp(self):
191+ # This test needs the privileges of rosettaadmin (to update
192+ # TranslationMessages) but it also needs to set up test conditions
193+ # which requires other privileges.
194+ self.layer.switchDbUser('postgres')
195+ super(TestMigrateCurrentFlag, self).setUp(user='mark@example.com')
196+ self.migrate_process = MigrateCurrentFlagProcess(self.layer.txn)
197+
198+ def test_getProductsWithTemplates_sampledata(self):
199+ # Sample data already has 3 products with templates.
200+ sampledata_products = list(
201+ self.migrate_process.getProductsWithTemplates())
202+ self.assertEquals(3, len(sampledata_products))
203+
204+ def test_getProductsWithTemplates_noop(self):
205+ # Adding a product with no templates doesn't change anything.
206+ sampledata_products = list(
207+ self.migrate_process.getProductsWithTemplates())
208+ self.factory.makeProduct()
209+ products = self.migrate_process.getProductsWithTemplates()
210+ self.assertContentEqual(sampledata_products, list(products))
211+
212+ def test_getProductsWithTemplates_new_template(self):
213+ # A new product with a template is included.
214+ sampledata_products = list(
215+ self.migrate_process.getProductsWithTemplates())
216+ product = self.factory.makeProduct()
217+ self.factory.makePOTemplate(productseries=product.development_focus)
218+ products = self.migrate_process.getProductsWithTemplates()
219+ self.assertContentEqual(
220+ sampledata_products + [product], list(products))
221+
222+ def test_getCurrentNonimportedTranslations_empty(self):
223+ # For a product with no translations no messages are returned.
224+ potemplate = self.factory.makePOTemplate()
225+ results = list(
226+ self.migrate_process.getCurrentNonimportedTranslations(
227+ potemplate.productseries.product))
228+ self.assertContentEqual([], results)
229+
230+ def test_getCurrentNonimportedTranslations_noncurrent(self):
231+ # For a product with non-current translations no messages
232+ # are returned.
233+ potemplate = self.factory.makePOTemplate()
234+ potmsgset = self.factory.makePOTMsgSet(
235+ potemplate=potemplate,
236+ sequence=1)
237+ pofile = self.factory.makePOFile(potemplate=potemplate)
238+ translation = self.factory.makeTranslationMessage(
239+ pofile=pofile, potmsgset=potmsgset, suggestion=True)
240+ results = list(
241+ self.migrate_process.getCurrentNonimportedTranslations(
242+ potemplate.productseries.product))
243+ self.assertContentEqual([], results)
244+
245+ def test_getCurrentNonimportedTranslations_current_imported(self):
246+ # For a product with current, imported translations no messages
247+ # are returned.
248+ potemplate = self.factory.makePOTemplate()
249+ potmsgset = self.factory.makePOTMsgSet(
250+ potemplate=potemplate,
251+ sequence=1)
252+ pofile = self.factory.makePOFile(potemplate=potemplate)
253+ translation = self.factory.makeTranslationMessage(
254+ pofile=pofile, potmsgset=potmsgset, is_imported=True)
255+ results = list(
256+ self.migrate_process.getCurrentNonimportedTranslations(
257+ potemplate.productseries.product))
258+ self.assertContentEqual([], results)
259+
260+ def test_getCurrentNonimportedTranslations_current_nonimported(self):
261+ # For a product with current, non-imported translations,
262+ # that translation is returned.
263+ potemplate = self.factory.makePOTemplate()
264+ potmsgset = self.factory.makePOTMsgSet(
265+ potemplate=potemplate,
266+ sequence=1)
267+ pofile = self.factory.makePOFile(potemplate=potemplate)
268+ translation = self.factory.makeTranslationMessage(
269+ pofile=pofile, potmsgset=potmsgset, is_imported=False)
270+ results = list(
271+ self.migrate_process.getCurrentNonimportedTranslations(
272+ potemplate.productseries.product))
273+ self.assertContentEqual([translation.id], results)
274+
275+
276+class TestUpdaterLoop(TestCaseWithFactory):
277+ """Test updater-loop core functionality."""
278+ layer = LaunchpadZopelessLayer
279+
280+ def setUp(self):
281+ # This test needs the privileges of rosettaadmin (to update
282+ # TranslationMessages) but it also needs to set up test conditions
283+ # which requires other privileges.
284+ self.layer.switchDbUser('postgres')
285+ super(TestUpdaterLoop, self).setUp(user='mark@example.com')
286+ self.logger = logging.getLogger("migrate-current-flag")
287+ self.migrate_loop = TranslationMessageImportedFlagUpdater(
288+ self.layer.txn, self.logger, [])
289+
290+ def test_updateTranslationMessages_base(self):
291+ # Passing in a TranslationMessage.id sets is_imported flag
292+ # on that message even if it was not set before.
293+ translation = self.factory.makeTranslationMessage()
294+ self.assertFalse(translation.is_imported)
295+
296+ self.migrate_loop._updateTranslationMessages([translation.id])
297+ self.assertTrue(translation.is_imported)
298+
299+ def test_updateTranslationMessages_unsetting_imported(self):
300+ # If there was a previous imported message, it is unset
301+ # first.
302+ pofile = self.factory.makePOFile()
303+ imported = self.factory.makeTranslationMessage(
304+ pofile=pofile, is_imported=True)
305+ translation = self.factory.makeTranslationMessage(
306+ pofile=pofile, potmsgset=imported.potmsgset, is_imported=False)
307+ self.assertTrue(imported.is_imported)
308+ self.assertFalse(imported.is_current)
309+ self.assertFalse(translation.is_imported)
310+ self.assertTrue(translation.is_current)
311+
312+ self.migrate_loop._updateTranslationMessages([translation.id])
313+ self.assertFalse(imported.is_imported)
314+ self.assertTrue(translation.is_imported)
315+ self.assertTrue(translation.is_current)
316+
317+ def test_updateTranslationMessages_other_language(self):
318+ # If there was a previous imported message in another language
319+ # it is not unset.
320+ pofile = self.factory.makePOFile()
321+ pofile_other = self.factory.makePOFile(potemplate=pofile.potemplate)
322+ imported = self.factory.makeTranslationMessage(
323+ pofile=pofile_other, is_imported=True)
324+ translation = self.factory.makeTranslationMessage(
325+ pofile=pofile, potmsgset=imported.potmsgset, is_imported=False)
326+ self.assertTrue(imported.is_imported)
327+ self.assertTrue(imported.is_current)
328+ self.assertFalse(translation.is_imported)
329+ self.assertTrue(translation.is_current)
330+
331+ self.migrate_loop._updateTranslationMessages([translation.id])
332+ self.assertTrue(imported.is_imported)
333+ self.assertTrue(imported.is_current)
334+ self.assertTrue(translation.is_imported)
335+ self.assertTrue(translation.is_current)
336+
337+ def test_updateTranslationMessages_diverged(self):
338+ # If there was a previous diverged message, it is not
339+ # touched.
340+ pofile = self.factory.makePOFile()
341+ translation = self.factory.makeTranslationMessage(
342+ pofile=pofile, is_imported=False)
343+ diverged_imported = self.factory.makeTranslationMessage(
344+ pofile=pofile, force_diverged=True, is_imported=True,
345+ potmsgset=translation.potmsgset)
346+ self.assertEquals(pofile.potemplate, diverged_imported.potemplate)
347+ self.assertTrue(diverged_imported.is_imported)
348+ self.assertTrue(diverged_imported.is_current)
349+
350+ self.migrate_loop._updateTranslationMessages([translation.id])
351+ self.assertEquals(pofile.potemplate, diverged_imported.potemplate)
352+ self.assertTrue(diverged_imported.is_imported)
353+ self.assertTrue(diverged_imported.is_current)
354
355=== added file 'scripts/rosetta/migrate_current_flag.py'
356--- scripts/rosetta/migrate_current_flag.py 1970-01-01 00:00:00 +0000
357+++ scripts/rosetta/migrate_current_flag.py 2010-11-22 23:19:36 +0000
358@@ -0,0 +1,30 @@
359+#!/usr/bin/python -S
360+#
361+# Copyright 2010 Canonical Ltd. This software is licensed under the
362+# GNU Affero General Public License version 3 (see the file LICENSE).
363+
364+"""Migrate current flag to imported flag on project translations."""
365+
366+import _pythonpath
367+
368+from lp.services.scripts.base import LaunchpadScript
369+from lp.translations.scripts.migrate_current_flag import (
370+ MigrateCurrentFlagProcess)
371+
372+
373+class MigrateTranslationFlags(LaunchpadScript):
374+ """Go through all POFiles and TranslationMessages and get rid of variants.
375+
376+ Replaces use of `variant` field with a new language with the code
377+ corresponding to the 'previous language'@'variant'.
378+ """
379+
380+ def main(self):
381+ fixer = MigrateCurrentFlagProcess(self.txn, self.logger)
382+ fixer.run()
383+
384+
385+if __name__ == '__main__':
386+ script = MigrateTranslationFlags(
387+ name="migratecurrentflag", dbuser='rosettaadmin')
388+ script.lock_and_run()