Merge lp:~danilo/launchpad/bug-814580 into lp:launchpad
- bug-814580
- Merge into devel
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 |
Related bugs: |
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 TranslationShar
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 translationpack
== Demo and Q/A ==
Rename a PO template, note how TranslationShar
Test how the job works by executing cronscripts/
(also move the po template to a different project/
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/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
./lib/lp/
571: local variable 'total_ids' is assigned to but never used
./lib/lp/
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/
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_
112: local variable 'upstream_
143: local variable 'upstream_message' is assigned to but never used
Данило Шеган (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
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 | + ) |
Looks good to me. ger.py" and "translationspl itter.py" should not be in the same directory?
I just wonder if "translationmer