Merge lp:~danilo/launchpad/bug-814580 into lp:launchpad

Proposed by Данило Шеган
Status: Merged
Merged at revision: 13902
Proposed branch: lp:~danilo/launchpad/bug-814580
Merge into: lp:launchpad
Prerequisite: lp:~danilo/launchpad/bug-814580-db
Diff against target: 788 lines (+531/-51)
8 files modified
lib/lp/translations/configure.zcml (+8/-0)
lib/lp/translations/interfaces/translationsharingjob.py (+3/-0)
lib/lp/translations/model/translationpackagingjob.py (+34/-1)
lib/lp/translations/model/translationsharingjob.py (+39/-3)
lib/lp/translations/tests/test_translationpackagingjob.py (+88/-11)
lib/lp/translations/tests/test_translationsplitter.py (+181/-0)
lib/lp/translations/translationmerger.py (+20/-0)
lib/lp/translations/utilities/translationsplitter.py (+158/-36)
To merge this branch: bzr merge lp:~danilo/launchpad/bug-814580
Reviewer Review Type Date Requested Status
Henning Eggers (community) Approve
Review via email: mp+69978@code.launchpad.net

Commit message

Merge or split messages when translation templates are renamed or moved to a different product or source package.

Description of the change

= Bug 814580: migrate translations on POTemplate changes =

== Proposed fix ==

This extends the TranslationSharingJob to do translation splitting/merging even when a single template is modified (i.e. renamed or moved to a different parent).
We use the existing code for translation merging and only add new code for finding which templates are affected, and do the similar thing for translation splitting.

I haven't fixed the lint yet so as not to make the diff larger. I'll happily do that before landing.

== Tests ==

bin/test -cvvt translationsplitter -t translationpackaging

== Demo and Q/A ==

Rename a PO template, note how TranslationSharingJob is created (table PackagingJob, in the process of being renamed).
Test how the job works by executing cronscripts/run_jobs.py -vv packaging_translations

(also move the po template to a different project/sourcepackage and confirm the same happens)

Use the POTemplate:+admin page to rename it or move it to a different project/source package.

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/translations/utilities/translationsplitter.py
  lib/lp/translations/interfaces/translationsharingjob.py
  lib/lp/translations/translationmerger.py
  lib/lp/translations/model/translationsharingjob.py
  lib/lp/translations/model/translationpackagingjob.py
  lib/lp/translations/tests/test_translationpackagingjob.py
  lib/lp/translations/configure.zcml
  lib/lp/translations/tests/test_translationsplitter.py

./lib/lp/translations/translationmerger.py
     571: local variable 'total_ids' is assigned to but never used
./lib/lp/translations/tests/test_translationpackagingjob.py
      63: local variable 'package' is assigned to but never used
     196: local variable 'recorder' is assigned to but never used
     207: local variable 'recorder' is assigned to but never used
     215: local variable 'other_packaging' is assigned to but never used
     229: local variable 'job' is assigned to but never used
     227: local variable 'recorder' is assigned to but never used
     236: local variable 'recorder' is assigned to but never used
     253: local variable 'recorder' is assigned to but never used
     257: local variable 'job2' is assigned to but never used
./lib/lp/translations/tests/test_translationsplitter.py
      81: local variable 'ubuntu_template' is assigned to but never used
      82: local variable 'ubuntu_sequence' is assigned to but never used
      95: local variable 'upstream_translation' is assigned to but never used
     112: local variable 'upstream_translation' is assigned to but never used
     143: local variable 'upstream_message' is assigned to but never used

To post a comment you must log in.
Revision history for this message
Henning Eggers (henninge) wrote :

Looks good to me.
I just wonder if "translationmerger.py" and "translationsplitter.py" should not be in the same directory?

review: Approve
Revision history for this message
Данило Шеган (danilo) wrote :

They should be, imho as well. However, these branches have taken enough of my time already.

Also, with the new db patch deployment process, I'll still have to land this on devel myself once db patch gets deployed, so keeping it as 'work in progress' and will land this MP directly once possible.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/translations/configure.zcml'
2--- lib/lp/translations/configure.zcml 2011-08-01 12:02:47 +0000
3+++ lib/lp/translations/configure.zcml 2011-08-01 12:02:49 +0000
4@@ -154,6 +154,10 @@
5 for="lp.registry.interfaces.packaging.IPackaging
6 lazr.lifecycle.interfaces.IObjectEvent"
7 handler=".model.translationsharingjob.schedule_packaging_job" />
8+ <subscriber
9+ for="lp.translations.interfaces.potemplate.IPOTemplate
10+ lazr.lifecycle.interfaces.IObjectModifiedEvent"
11+ handler=".model.translationsharingjob.schedule_potemplate_job" />
12 <facet
13 facet="translations">
14
15@@ -643,6 +647,10 @@
16 class="lp.translations.model.translationpackagingjob.TranslationSplitJob">
17 <allow interface='lp.services.job.interfaces.job.IRunnableJob'/>
18 </class>
19+ <class
20+ class="lp.translations.model.translationpackagingjob.TranslationTemplateChangeJob">
21+ <allow interface='lp.services.job.interfaces.job.IRunnableJob'/>
22+ </class>
23 <utility
24 component="lp.translations.model.translationtemplatesbuildjob.TranslationTemplatesBuildJob"
25 provides="lp.buildmaster.interfaces.buildfarmjob.IBuildFarmJob"
26
27=== modified file 'lib/lp/translations/interfaces/translationsharingjob.py'
28--- lib/lp/translations/interfaces/translationsharingjob.py 2011-08-01 12:02:47 +0000
29+++ lib/lp/translations/interfaces/translationsharingjob.py 2011-08-01 12:02:49 +0000
30@@ -19,3 +19,6 @@
31
32 sourcepackagename = Attribute(
33 _("The sourcepackagename of the Packaging."))
34+
35+ potemplate = Attribute(
36+ _("The POTemplate to pass around as the relevant template."))
37
38=== modified file 'lib/lp/translations/model/translationpackagingjob.py'
39--- lib/lp/translations/model/translationpackagingjob.py 2011-08-01 12:02:47 +0000
40+++ lib/lp/translations/model/translationpackagingjob.py 2011-08-01 12:02:49 +0000
41@@ -10,6 +10,7 @@
42 __all__ = [
43 'TranslationMergeJob',
44 'TranslationSplitJob',
45+ 'TranslationTemplateChangeJob',
46 ]
47
48 import logging
49@@ -17,6 +18,7 @@
50 from lazr.lifecycle.interfaces import (
51 IObjectCreatedEvent,
52 IObjectDeletedEvent,
53+ IObjectModifiedEvent,
54 )
55 import transaction
56 from zope.interface import (
57@@ -40,7 +42,10 @@
58 TransactionManager,
59 TranslationMerger,
60 )
61-from lp.translations.utilities.translationsplitter import TranslationSplitter
62+from lp.translations.utilities.translationsplitter import (
63+ TranslationSplitter,
64+ TranslationTemplateSplitter,
65+ )
66
67
68 class TranslationPackagingJob(TranslationSharingJobDerived, BaseRunnableJob):
69@@ -117,3 +122,31 @@
70 'Splitting %s and %s', self.productseries.displayname,
71 self.sourcepackage.displayname)
72 TranslationSplitter(self.productseries, self.sourcepackage).split()
73+
74+
75+class TranslationTemplateChangeJob(TranslationPackagingJob):
76+ """Job for merging/splitting translations when template is changed."""
77+
78+ implements(IRunnableJob)
79+
80+ class_job_type = TranslationSharingJobType.TEMPLATE_CHANGE
81+
82+ create_on_event = IObjectModifiedEvent
83+
84+ @classmethod
85+ def forPOTemplate(cls, potemplate):
86+ """Create a TranslationTemplateChangeJob for a POTemplate.
87+
88+ :param potemplate: The `POTemplate` to create the job for.
89+ :return: A `TranslationTemplateChangeJob`.
90+ """
91+ return cls.create(potemplate=potemplate)
92+
93+ def run(self):
94+ """See `IRunnableJob`."""
95+ logger = logging.getLogger()
96+ logger.info("Sanitizing translations for '%s'" % (
97+ self.potemplate.displayname))
98+ TranslationTemplateSplitter(self.potemplate).split()
99+ tm = TransactionManager(transaction.manager, False)
100+ TranslationMerger.mergeModifiedTemplates(self.potemplate, tm)
101
102=== modified file 'lib/lp/translations/model/translationsharingjob.py'
103--- lib/lp/translations/model/translationsharingjob.py 2011-08-01 12:02:47 +0000
104+++ lib/lp/translations/model/translationsharingjob.py 2011-08-01 12:02:49 +0000
105@@ -37,6 +37,7 @@
106 from lp.translations.interfaces.translationsharingjob import (
107 ITranslationSharingJob,
108 )
109+from lp.translations.model.potemplate import POTemplate
110
111
112 class TranslationSharingJobType(DBEnumeratedType):
113@@ -54,6 +55,12 @@
114 Split translations between productseries and sourcepackage.
115 """)
116
117+ TEMPLATE_CHANGE = DBItem(2, """
118+ Split/merge translations for a single translation template.
119+
120+ Split/merge translations for a single translation template.
121+ """)
122+
123
124 class TranslationSharingJob(StormBase):
125 """Base class for jobs related to a packaging."""
126@@ -82,8 +89,12 @@
127
128 sourcepackagename = Reference(sourcepackagename_id, SourcePackageName.id)
129
130+ potemplate_id = Int('potemplate')
131+
132+ potemplate = Reference(potemplate_id, POTemplate.id)
133+
134 def __init__(self, job, job_type, productseries, distroseries,
135- sourcepackagename):
136+ sourcepackagename, potemplate=None):
137 """"Constructor.
138
139 :param job: The `Job` to use for storing basic job state.
140@@ -96,6 +107,7 @@
141 self.distroseries = distroseries
142 self.sourcepackagename = sourcepackagename
143 self.productseries = productseries
144+ self.potemplate = potemplate
145
146
147 class RegisteredSubclass(type):
148@@ -143,16 +155,18 @@
149 self.job = job
150
151 @classmethod
152- def create(cls, productseries, distroseries, sourcepackagename):
153+ def create(cls, productseries=None, distroseries=None,
154+ sourcepackagename=None, potemplate=None):
155 """"Create a TranslationPackagingJob backed by TranslationSharingJob.
156
157 :param productseries: The ProductSeries side of the Packaging.
158 :param distroseries: The distroseries of the Packaging sourcepackage.
159 :param sourcepackagename: The name of the Packaging sourcepackage.
160+ :param potemplate: POTemplate to restrict to (if any).
161 """
162 context = TranslationSharingJob(
163 Job(), cls.class_job_type, productseries,
164- distroseries, sourcepackagename)
165+ distroseries, sourcepackagename, potemplate)
166 return cls(context)
167
168 @classmethod
169@@ -170,6 +184,27 @@
170 job_class.forPackaging(packaging)
171
172 @classmethod
173+ def schedulePOTemplateJob(cls, potemplate, event):
174+ """Event subscriber to create a TranslationSharingJob on events.
175+
176+ :param potemplate: The `POTemplate` to create
177+ a `TranslationSharingJob` for.
178+ :param event: The event itself.
179+ """
180+ if ('name' not in event.edited_fields and
181+ 'productseries' not in event.edited_fields and
182+ 'distroseries' not in event.edited_fields and
183+ 'sourcepackagename' not in event.edited_fields):
184+ # Ignore changes to POTemplates that are neither renames,
185+ # nor moves to a different package/project.
186+ return
187+ for event_type, job_classes in cls._event_types.iteritems():
188+ if not event_type.providedBy(event):
189+ continue
190+ for job_class in job_classes:
191+ job_class.forPOTemplate(potemplate)
192+
193+ @classmethod
194 def iterReady(cls, extra_clauses):
195 """See `IJobSource`.
196
197@@ -207,3 +242,4 @@
198
199 #make accessible to zcml
200 schedule_packaging_job = TranslationSharingJobDerived.schedulePackagingJob
201+schedule_potemplate_job = TranslationSharingJobDerived.schedulePOTemplateJob
202
203=== modified file 'lib/lp/translations/tests/test_translationpackagingjob.py'
204--- lib/lp/translations/tests/test_translationpackagingjob.py 2011-08-01 12:02:47 +0000
205+++ lib/lp/translations/tests/test_translationpackagingjob.py 2011-08-01 12:02:49 +0000
206@@ -8,12 +8,17 @@
207
208 import transaction
209 from zope.component import getUtility
210+from zope.event import notify
211+
212+from lazr.lifecycle.event import ObjectModifiedEvent
213+from lazr.lifecycle.snapshot import Snapshot
214
215 from canonical.launchpad.webapp.testing import verifyObject
216 from canonical.testing.layers import (
217 LaunchpadZopelessLayer,
218 )
219 from lp.registry.interfaces.packaging import IPackagingUtil
220+from lp.translations.interfaces.potemplate import IPOTemplate
221 from lp.translations.model.translationsharingjob import (
222 TranslationSharingJob,
223 TranslationSharingJobDerived,
224@@ -36,6 +41,7 @@
225 TranslationMergeJob,
226 TranslationPackagingJob,
227 TranslationSplitJob,
228+ TranslationTemplateChangeJob,
229 )
230 from lp.translations.tests.test_translationsplitter import (
231 make_shared_potmsgset,
232@@ -101,20 +107,32 @@
233
234 class JobFinder:
235
236- def __init__(self, productseries, sourcepackage, job_class):
237- self.productseries = productseries
238- self.sourcepackagename = sourcepackage.sourcepackagename
239- self.distroseries = sourcepackage.distroseries
240+ def __init__(self, productseries, sourcepackage, job_class,
241+ potemplate=None):
242+ if potemplate is None:
243+ self.productseries = productseries
244+ self.sourcepackagename = sourcepackage.sourcepackagename
245+ self.distroseries = sourcepackage.distroseries
246+ self.potemplate = None
247+ else:
248+ self.potemplate = potemplate
249 self.job_type = job_class.class_job_type
250
251 def find(self):
252- return list(TranslationSharingJobDerived.iterReady([
253- TranslationSharingJob.productseries_id == self.productseries.id,
254- (TranslationSharingJob.sourcepackagename_id ==
255- self.sourcepackagename.id),
256- TranslationSharingJob.distroseries_id == self.distroseries.id,
257- TranslationSharingJob.job_type == self.job_type,
258- ]))
259+ if self.potemplate is None:
260+ return list(TranslationSharingJobDerived.iterReady([
261+ TranslationSharingJob.productseries_id == self.productseries.id,
262+ (TranslationSharingJob.sourcepackagename_id ==
263+ self.sourcepackagename.id),
264+ TranslationSharingJob.distroseries_id == self.distroseries.id,
265+ TranslationSharingJob.job_type == self.job_type,
266+ ]))
267+ else:
268+ return list(
269+ TranslationSharingJobDerived.iterReady([
270+ TranslationSharingJob.potemplate_id == self.potemplate.id,
271+ TranslationSharingJob.job_type == self.job_type,
272+ ]))
273
274
275 class TestTranslationPackagingJob(TestCaseWithFactory):
276@@ -275,3 +293,62 @@
277 packaging.distroseries)
278 (job,) = finder.find()
279 self.assertIsInstance(job, TranslationSplitJob)
280+
281+
282+class TestTranslationTemplateChangeJob(TestCaseWithFactory):
283+
284+ layer = LaunchpadZopelessLayer
285+
286+ def test_modifyPOTemplate_makes_job(self):
287+ """Creating a Packaging should make a TranslationMergeJob."""
288+ potemplate = self.factory.makePOTemplate()
289+ finder = JobFinder(
290+ None, None, TranslationTemplateChangeJob, potemplate)
291+ self.assertEqual([], finder.find())
292+ with person_logged_in(potemplate.owner):
293+ snapshot = Snapshot(potemplate, providing=IPOTemplate)
294+ potemplate.name = self.factory.getUniqueString()
295+ notify(ObjectModifiedEvent(potemplate, snapshot, ["name"]))
296+
297+ (job,) = finder.find()
298+ self.assertIsInstance(job, TranslationTemplateChangeJob)
299+
300+ def test_splits_and_merges(self):
301+ """Changing a template makes the translations split and then
302+ re-merged in the new target sharing set."""
303+ potemplate = self.factory.makePOTemplate(name='template')
304+ other_ps = self.factory.makeProductSeries(
305+ product=potemplate.productseries.product)
306+ old_shared = self.factory.makePOTemplate(name='template',
307+ productseries=other_ps)
308+ new_shared = self.factory.makePOTemplate(name='renamed',
309+ productseries=other_ps)
310+
311+ # Set up shared POTMsgSets and translations.
312+ potmsgset = self.factory.makePOTMsgSet(potemplate, sequence=1)
313+ potmsgset.setSequence(old_shared, 1)
314+ self.factory.makeCurrentTranslationMessage(potmsgset=potmsgset)
315+
316+ # This is the identical English message in the new_shared template.
317+ target_potmsgset = self.factory.makePOTMsgSet(
318+ new_shared, sequence=1, singular=potmsgset.singular_text)
319+
320+ # Rename the template and confirm that messages are now shared
321+ # with new_shared instead of old_shared.
322+ potemplate.name = 'renamed'
323+ job = TranslationTemplateChangeJob.create(potemplate=potemplate)
324+
325+ self.becomeDbUser('rosettaadmin')
326+ job.run()
327+
328+ # New POTMsgSet is now different from the old one (it's been split),
329+ # but matches the target potmsgset (it's been merged into it).
330+ new_potmsgset = potemplate.getPOTMsgSets()[0]
331+ self.assertNotEqual(potmsgset, new_potmsgset)
332+ self.assertEqual(target_potmsgset, new_potmsgset)
333+
334+ # Translations have been merged as well.
335+ self.assertContentEqual(
336+ [tm.translations for tm in potmsgset.getAllTranslationMessages()],
337+ [tm.translations
338+ for tm in new_potmsgset.getAllTranslationMessages()])
339
340=== modified file 'lib/lp/translations/tests/test_translationsplitter.py'
341--- lib/lp/translations/tests/test_translationsplitter.py 2011-02-25 20:23:40 +0000
342+++ lib/lp/translations/tests/test_translationsplitter.py 2011-08-01 12:02:49 +0000
343@@ -13,6 +13,7 @@
344 )
345 from lp.translations.utilities.translationsplitter import (
346 TranslationSplitter,
347+ TranslationTemplateSplitter,
348 )
349
350
351@@ -153,3 +154,183 @@
352 upstream_item.potmsgset.getAllTranslationMessages().count(),
353 ubuntu_item.potmsgset.getAllTranslationMessages().count(),
354 )
355+
356+
357+class TestTranslationTemplateSplitterBase:
358+
359+ layer = ZopelessDatabaseLayer
360+
361+ def getPOTMsgSetAndTemplateToSplit(self, splitter):
362+ return [(tti1.potmsgset, tti1.potemplate)
363+ for tti1, tti2 in splitter.findShared()]
364+
365+ def setUpSharingTemplates(self, other_side=False):
366+ """Sets up two sharing templates with one sharing message and
367+ one non-sharing message in each template."""
368+ template1 = self.makePOTemplate()
369+ template2 = self.makeSharingTemplate(template1, other_side)
370+
371+ shared_potmsgset = self.factory.makePOTMsgSet(template1, sequence=1)
372+ shared_potmsgset.setSequence(template2, 1)
373+
374+ # POTMsgSets appearing in only one of the templates are not returned.
375+ self.factory.makePOTMsgSet(template1, sequence=2)
376+ self.factory.makePOTMsgSet(template2, sequence=2)
377+ return template1, template2, shared_potmsgset
378+
379+ def makePOTemplate(self):
380+ raise NotImplementedError('Subclasses should implement this.')
381+
382+ def makeSharingTemplate(self, template, other_side=False):
383+ raise NotImplementedError('Subclasses should implement this.')
384+
385+ def test_findShared_renamed(self):
386+ """Shared POTMsgSets are included for a renamed template."""
387+ template1, template2, shared_potmsgset = self.setUpSharingTemplates()
388+
389+ splitter = TranslationTemplateSplitter(template2)
390+ self.assertContentEqual([], splitter.findShared())
391+
392+ template2.name = 'renamed'
393+ self.assertContentEqual(
394+ [(shared_potmsgset, template1)],
395+ self.getPOTMsgSetAndTemplateToSplit(splitter))
396+
397+ def test_findShared_moved_product(self):
398+ """Moving a template to a different product splits its messages."""
399+ template1, template2, shared_potmsgset = self.setUpSharingTemplates()
400+
401+ splitter = TranslationTemplateSplitter(template2)
402+ self.assertContentEqual([], splitter.findShared())
403+
404+ # Move the template to a different product entirely.
405+ template2.productseries = self.factory.makeProduct().development_focus
406+ template2.distroseries = None
407+ template2.sourcepackagename = None
408+ self.assertContentEqual(
409+ [(shared_potmsgset, template1)],
410+ self.getPOTMsgSetAndTemplateToSplit(splitter))
411+
412+ def test_findShared_moved_distribution(self):
413+ """Moving a template to a different distribution gets it split."""
414+ template1, template2, shared_potmsgset = self.setUpSharingTemplates()
415+
416+ splitter = TranslationTemplateSplitter(template2)
417+ self.assertContentEqual([], splitter.findShared())
418+
419+ # Move the template to a different distribution entirely.
420+ sourcepackage = self.factory.makeSourcePackage()
421+ template2.distroseries = sourcepackage.distroseries
422+ template2.sourcepackagename = sourcepackage.sourcepackagename
423+ template2.productseries = None
424+ self.assertContentEqual(
425+ [(shared_potmsgset, template1)],
426+ self.getPOTMsgSetAndTemplateToSplit(splitter))
427+
428+ def test_findShared_moved_to_nonsharing_target(self):
429+ """Moving a template to a target not sharing with the existing
430+ upstreams and source package gets it split."""
431+ template1, template2, shared_potmsgset = self.setUpSharingTemplates(
432+ other_side=True)
433+
434+ splitter = TranslationTemplateSplitter(template2)
435+ self.assertContentEqual([], splitter.findShared())
436+
437+ # Move the template to a different distribution entirely.
438+ sourcepackage = self.factory.makeSourcePackage()
439+ template2.distroseries = sourcepackage.distroseries
440+ template2.sourcepackagename = sourcepackage.sourcepackagename
441+ template2.productseries = None
442+ self.assertContentEqual(
443+ [(shared_potmsgset, template1)],
444+ self.getPOTMsgSetAndTemplateToSplit(splitter))
445+
446+ def test_split_messages(self):
447+ """Splitting messages works properly."""
448+ template1, template2, shared_potmsgset = self.setUpSharingTemplates()
449+
450+ splitter = TranslationTemplateSplitter(template2)
451+ self.assertContentEqual([], splitter.findShared())
452+
453+ # Move the template to a different product entirely.
454+ template2.productseries = self.factory.makeProduct().development_focus
455+ template2.distroseries = None
456+ template2.sourcepackagename = None
457+
458+ other_item, this_item = splitter.findShared()[0]
459+
460+ splitter.split()
461+
462+ self.assertNotEqual(other_item.potmsgset, this_item.potmsgset)
463+ self.assertEqual(shared_potmsgset, other_item.potmsgset)
464+ self.assertNotEqual(shared_potmsgset, this_item.potmsgset)
465+
466+
467+class TestProductTranslationTemplateSplitter(
468+ TestCaseWithFactory, TestTranslationTemplateSplitterBase):
469+ """Templates in a product get split appropriately."""
470+
471+ def makePOTemplate(self):
472+ return self.factory.makePOTemplate(
473+ name='template',
474+ side=TranslationSide.UPSTREAM)
475+
476+ def makeSharingTemplate(self, template, other_side=False):
477+ if other_side:
478+ template2 = self.factory.makePOTemplate(
479+ name='template',
480+ side=TranslationSide.UBUNTU)
481+ self.factory.makePackagingLink(
482+ productseries=template.productseries,
483+ distroseries=template2.distroseries,
484+ sourcepackagename=template2.sourcepackagename)
485+ return template2
486+ else:
487+ product = template.productseries.product
488+ other_series = self.factory.makeProductSeries(product=product)
489+ return self.factory.makePOTemplate(name='template',
490+ productseries=other_series)
491+
492+
493+class TestDistributionTranslationTemplateSplitter(
494+ TestCaseWithFactory, TestTranslationTemplateSplitterBase):
495+ """Templates in a distribution get split appropriately."""
496+
497+ def makePOTemplate(self):
498+ return self.factory.makePOTemplate(
499+ name='template',
500+ side=TranslationSide.UBUNTU)
501+
502+ def makeSharingTemplate(self, template, other_side=False):
503+ if other_side:
504+ template2 = self.factory.makePOTemplate(
505+ name='template',
506+ side=TranslationSide.UPSTREAM)
507+ self.factory.makePackagingLink(
508+ productseries=template2.productseries,
509+ distroseries=template.distroseries,
510+ sourcepackagename=template.sourcepackagename)
511+ return template2
512+ else:
513+ distro = template.distroseries.distribution
514+ other_series = self.factory.makeDistroRelease(distribution=distro)
515+ return self.factory.makePOTemplate(
516+ name='template',
517+ distroseries=other_series,
518+ sourcepackagename=template.sourcepackagename)
519+
520+ def test_findShared_moved_sourcepackage(self):
521+ """Moving a template to a different source package gets it split."""
522+ template1, template2, shared_potmsgset = self.setUpSharingTemplates()
523+
524+ splitter = TranslationTemplateSplitter(template2)
525+ self.assertContentEqual([], splitter.findShared())
526+
527+ # Move the template to a different source package inside the
528+ # same distroseries.
529+ sourcepackage = self.factory.makeSourcePackage(
530+ distroseries=template2.distroseries)
531+ template2.sourcepackagename = sourcepackage.sourcepackagename
532+ self.assertContentEqual(
533+ [(shared_potmsgset, template1)],
534+ self.getPOTMsgSetAndTemplateToSplit(splitter))
535
536=== modified file 'lib/lp/translations/translationmerger.py'
537--- lib/lp/translations/translationmerger.py 2011-05-27 21:12:25 +0000
538+++ lib/lp/translations/translationmerger.py 2011-08-01 12:02:49 +0000
539@@ -387,6 +387,26 @@
540 merger = cls(templates, tm)
541 merger.mergePOTMsgSets()
542
543+ @classmethod
544+ def mergeModifiedTemplates(cls, potemplate, tm):
545+ subset = getUtility(IPOTemplateSet).getSharingSubset(
546+ distribution=potemplate.distribution,
547+ sourcepackagename=potemplate.sourcepackagename,
548+ product=potemplate.product)
549+ templates = list(subset.getSharingPOTemplates(potemplate.name))
550+ templates.sort(key=methodcaller('sharingKey'), reverse=True)
551+ merger = cls(templates, tm)
552+ merger.mergeAll()
553+
554+ def mergeAll(self):
555+ """Properly merge POTMsgSets and TranslationMessages."""
556+ self._removeDuplicateMessages()
557+ self.tm.endTransaction(intermediate=True)
558+ self.mergePOTMsgSets()
559+ self.tm.endTransaction(intermediate=True)
560+ self.mergeTranslationMessages()
561+ self.tm.endTransaction()
562+
563 def __init__(self, potemplates, tm):
564 """Constructor.
565
566
567=== modified file 'lib/lp/translations/utilities/translationsplitter.py'
568--- lib/lp/translations/utilities/translationsplitter.py 2011-05-12 20:21:58 +0000
569+++ lib/lp/translations/utilities/translationsplitter.py 2011-08-01 12:02:49 +0000
570@@ -6,50 +6,30 @@
571
572 import logging
573
574-from storm.locals import ClassAlias, Store
575+from storm.expr import (
576+ And,
577+ Join,
578+ LeftJoin,
579+ Not,
580+ Or,
581+ )
582+from storm.locals import (
583+ ClassAlias,
584+ Store,
585+ )
586 import transaction
587
588+from lp.registry.model.distroseries import DistroSeries
589+from lp.registry.model.packaging import Packaging
590+from lp.registry.model.productseries import ProductSeries
591 from lp.translations.model.potemplate import POTemplate
592 from lp.translations.model.translationtemplateitem import (
593 TranslationTemplateItem,
594 )
595
596
597-class TranslationSplitter:
598- """Split translations for a productseries, sourcepackage pair.
599-
600- If a productseries and sourcepackage were linked in error, and then
601- unlinked, they may still share some translations. This class breaks those
602- associations.
603- """
604-
605- def __init__(self, productseries, sourcepackage):
606- """Constructor.
607-
608- :param productseries: The `ProductSeries` to split from.
609- :param sourcepackage: The `SourcePackage` to split from.
610- """
611- self.productseries = productseries
612- self.sourcepackage = sourcepackage
613-
614- def findShared(self):
615- """Provide tuples of upstream, ubuntu for each shared POTMsgSet."""
616- store = Store.of(self.productseries)
617- UpstreamItem = ClassAlias(TranslationTemplateItem, 'UpstreamItem')
618- UpstreamTemplate = ClassAlias(POTemplate, 'UpstreamTemplate')
619- UbuntuItem = ClassAlias(TranslationTemplateItem, 'UbuntuItem')
620- UbuntuTemplate = ClassAlias(POTemplate, 'UbuntuTemplate')
621- return store.find(
622- (UpstreamItem, UbuntuItem),
623- UpstreamItem.potmsgsetID == UbuntuItem.potmsgsetID,
624- UbuntuItem.potemplateID == UbuntuTemplate.id,
625- UbuntuTemplate.sourcepackagenameID ==
626- self.sourcepackage.sourcepackagename.id,
627- UbuntuTemplate.distroseriesID ==
628- self.sourcepackage.distroseries.id,
629- UpstreamItem.potemplateID == UpstreamTemplate.id,
630- UpstreamTemplate.productseriesID == self.productseries.id,
631- )
632+class TranslationSplitterBase:
633+ """Base class for translation splitting jobs."""
634
635 @staticmethod
636 def splitPOTMsgSet(ubuntu_item):
637@@ -86,9 +66,151 @@
638 """Split the translations for the ProductSeries and SourcePackage."""
639 logger = logging.getLogger()
640 shared = enumerate(self.findShared(), 1)
641+ total = 0
642 for num, (upstream_item, ubuntu_item) in shared:
643 self.splitPOTMsgSet(ubuntu_item)
644 self.migrateTranslations(upstream_item.potmsgset, ubuntu_item)
645 if num % 100 == 0:
646 logger.info('%d entries split. Committing...', num)
647 transaction.commit()
648+ total = num
649+
650+ if total % 100 != 0 or total == 0:
651+ transaction.commit()
652+ logger.info('%d entries split.', total)
653+
654+
655+class TranslationSplitter(TranslationSplitterBase):
656+ """Split translations for a productseries, sourcepackage pair.
657+
658+ If a productseries and sourcepackage were linked in error, and then
659+ unlinked, they may still share some translations. This class breaks those
660+ associations.
661+ """
662+
663+ def __init__(self, productseries, sourcepackage):
664+ """Constructor.
665+
666+ :param productseries: The `ProductSeries` to split from.
667+ :param sourcepackage: The `SourcePackage` to split from.
668+ """
669+ self.productseries = productseries
670+ self.sourcepackage = sourcepackage
671+
672+ def findShared(self):
673+ """Provide tuples of upstream, ubuntu for each shared POTMsgSet."""
674+ store = Store.of(self.productseries)
675+ UpstreamItem = ClassAlias(TranslationTemplateItem, 'UpstreamItem')
676+ UpstreamTemplate = ClassAlias(POTemplate, 'UpstreamTemplate')
677+ UbuntuItem = ClassAlias(TranslationTemplateItem, 'UbuntuItem')
678+ UbuntuTemplate = ClassAlias(POTemplate, 'UbuntuTemplate')
679+ return store.find(
680+ (UpstreamItem, UbuntuItem),
681+ UpstreamItem.potmsgsetID == UbuntuItem.potmsgsetID,
682+ UbuntuItem.potemplateID == UbuntuTemplate.id,
683+ UbuntuTemplate.sourcepackagenameID ==
684+ self.sourcepackage.sourcepackagename.id,
685+ UbuntuTemplate.distroseriesID ==
686+ self.sourcepackage.distroseries.id,
687+ UpstreamItem.potemplateID == UpstreamTemplate.id,
688+ UpstreamTemplate.productseriesID == self.productseries.id,
689+ )
690+
691+
692+class TranslationTemplateSplitter(TranslationSplitterBase):
693+ """Split translations for an extracted potemplate.
694+
695+ When a POTemplate is removed from a set of sharing templates,
696+ it keeps sharing POTMsgSets with other templates. This class
697+ removes those associations.
698+ """
699+
700+ def __init__(self, potemplate):
701+ """Constructor.
702+
703+ :param potemplate: The `POTemplate` to sanitize.
704+ """
705+ self.potemplate = potemplate
706+
707+ def findShared(self):
708+ """Provide tuples of (other, this) items for each shared POTMsgSet.
709+
710+ Only return those that are shared but shouldn't be because they
711+ are now in non-sharing templates.
712+ """
713+ store = Store.of(self.potemplate)
714+ ThisItem = ClassAlias(TranslationTemplateItem, 'ThisItem')
715+ OtherItem = ClassAlias(TranslationTemplateItem, 'OtherItem')
716+ OtherTemplate = ClassAlias(POTemplate, 'OtherTemplate')
717+
718+ tables = [
719+ OtherTemplate,
720+ Join(OtherItem, OtherItem.potemplateID == OtherTemplate.id),
721+ Join(ThisItem,
722+ And(ThisItem.potmsgsetID == OtherItem.potmsgsetID,
723+ ThisItem.potemplateID == self.potemplate.id)),
724+ ]
725+
726+ if self.potemplate.productseries is not None:
727+ # If the template is now in a product, we look for all
728+ # effectively sharing templates that are in *different*
729+ # products, or that are in a sourcepackage which is not
730+ # linked (through Packaging table) with this product.
731+ ps = self.potemplate.productseries
732+ productseries_join = LeftJoin(
733+ ProductSeries,
734+ ProductSeries.id == OtherTemplate.productseriesID)
735+ packaging_join = LeftJoin(
736+ Packaging,
737+ And(Packaging.productseriesID == ps.id,
738+ (Packaging.sourcepackagenameID ==
739+ OtherTemplate.sourcepackagenameID),
740+ Packaging.distroseriesID == OtherTemplate.distroseriesID
741+ ))
742+ tables.extend([productseries_join, packaging_join])
743+ # Template should not be sharing if...
744+ other_clauses = Or(
745+ # The name is different, or...
746+ OtherTemplate.name != self.potemplate.name,
747+ # It's in a different product, or...
748+ And(Not(ProductSeries.id == None),
749+ ProductSeries.productID != ps.productID),
750+ # There is no link between this product series and
751+ # a source package the template is in.
752+ And(Not(OtherTemplate.distroseriesID == None),
753+ Packaging.id == None))
754+ else:
755+ # If the template is now in a source package, we look for all
756+ # effectively sharing templates that are in *different*
757+ # distributions or source packages, or that are in a product
758+ # which is not linked with this source package.
759+ ds = self.potemplate.distroseries
760+ spn = self.potemplate.sourcepackagename
761+ distroseries_join = LeftJoin(
762+ DistroSeries,
763+ DistroSeries.id == OtherTemplate.distroseriesID)
764+ packaging_join = LeftJoin(
765+ Packaging,
766+ And(Packaging.distroseriesID == ds.id,
767+ Packaging.sourcepackagenameID == spn.id,
768+ Packaging.productseriesID == OtherTemplate.productseriesID
769+ ))
770+ tables.extend([distroseries_join, packaging_join])
771+ # Template should not be sharing if...
772+ other_clauses = Or(
773+ # The name is different, or...
774+ OtherTemplate.name != self.potemplate.name,
775+ # It's in a different distribution or source package, or...
776+ And(Not(DistroSeries.id == None),
777+ Or(DistroSeries.distributionID != ds.distributionID,
778+ OtherTemplate.sourcepackagenameID != spn.id)),
779+ # There is no link between this source package and
780+ # a product the template is in.
781+ And(Not(OtherTemplate.productseriesID == None),
782+ Packaging.id == None))
783+
784+ return store.using(*tables).find(
785+ (OtherItem, ThisItem),
786+ OtherTemplate.id != self.potemplate.id,
787+ other_clauses,
788+ )