Merge lp:~henninge/launchpad/recife-poimport into lp:~launchpad/launchpad/recife
- recife-poimport
- Merge into recife
Status: | Merged |
---|---|
Merged at revision: | 9169 |
Proposed branch: | lp:~henninge/launchpad/recife-poimport |
Merge into: | lp:~launchpad/launchpad/recife |
Diff against target: |
1034 lines (+425/-383) 3 files modified
lib/lp/testing/factory.py (+10/-0) lib/lp/translations/doc/poimport-script.txt (+342/-0) lib/lp/translations/doc/poimport.txt (+73/-383) |
To merge this branch: | bzr merge lp:~henninge/launchpad/recife-poimport |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Māris Fogels (community) | Approve | ||
Review via email: mp+36165@code.launchpad.net |
Commit message
Description of the change
== Details ==
This branch was split off the work for bug 611674. There were two goals:
- Make the test use a source package because a lot of code still hardwired to "is_current_
- Update the test to not use (or at least not as much) sample data.
Because the file was quite big, I split off the tests that run the import script into poimport-
Since the new model is explicitly referring to Ubuntu in many places, I found it useful to have a "makeUbuntuDist
== Test ==
bin/test -vvcm lp.translations -t poimport.txt -t poimport-script.txt
Preview Diff
1 | === modified file 'lib/lp/testing/factory.py' |
2 | --- lib/lp/testing/factory.py 2010-09-08 02:19:20 +0000 |
3 | +++ lib/lp/testing/factory.py 2010-09-21 16:23:42 +0000 |
4 | @@ -1848,8 +1848,18 @@ |
5 | series.status = status |
6 | return ProxyFactory(series) |
7 | |
8 | + def makeUbuntuDistroRelease(self, version=None, |
9 | + status=SeriesStatus.DEVELOPMENT, |
10 | + parent_series=None, name=None, |
11 | + displayname=None): |
12 | + """Short cut to use the celebrity 'ubuntu' as the distribution.""" |
13 | + ubuntu = getUtility(ILaunchpadCelebrities).ubuntu |
14 | + return self.makeDistroRelease( |
15 | + ubuntu, version, status, parent_series, name, displayname) |
16 | + |
17 | # Most people think of distro releases as distro series. |
18 | makeDistroSeries = makeDistroRelease |
19 | + makeUbuntuDistroSeries = makeUbuntuDistroRelease |
20 | |
21 | def makeDistroSeriesDifference( |
22 | self, derived_series=None, source_package_name_str=None, |
23 | |
24 | === added file 'lib/lp/translations/doc/poimport-script.txt' |
25 | --- lib/lp/translations/doc/poimport-script.txt 1970-01-01 00:00:00 +0000 |
26 | +++ lib/lp/translations/doc/poimport-script.txt 2010-09-21 16:23:42 +0000 |
27 | @@ -0,0 +1,342 @@ |
28 | +Import Script |
29 | +============= |
30 | + |
31 | +The imports are performed by a dedicated cron script. |
32 | + |
33 | +A template and two pofile will be imported. |
34 | + |
35 | + >>> potemplate_header = r""" |
36 | + ... msgid "" |
37 | + ... msgstr "" |
38 | + ... "POT-Creation-Date: 2004-07-11 16:16+0900\n" |
39 | + ... "Content-Type: text/plain; charset=CHARSET\n" |
40 | + ... "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" |
41 | + ... |
42 | + ... """ |
43 | + |
44 | + >>> pofile_header = r""" |
45 | + ... msgid "" |
46 | + ... msgstr "" |
47 | + ... "PO-Revision-Date: 2005-06-03 20:41+0100\n" |
48 | + ... "Last-Translator: Foo <no-priv@canonical.com>\n" |
49 | + ... "Content-Type: text/plain; charset=UTF-8\n" |
50 | + ... "Plural-Forms: nplurals=2; plural=(n!=1);\n" |
51 | + ... |
52 | + ... """ |
53 | + |
54 | + >>> po_content = r""" |
55 | + ... #: test.c:13 |
56 | + ... msgid "baz" |
57 | + ... msgstr "%s" |
58 | + ... |
59 | + ... #, c-format |
60 | + ... msgid "Foo %%s" |
61 | + ... msgstr "%s" |
62 | + ... |
63 | + ... #, c-format |
64 | + ... msgid "Singular %%d" |
65 | + ... msgid_plural "Plural %%d" |
66 | + ... msgstr[0] "%s" |
67 | + ... msgstr[1] "%s" |
68 | + ... |
69 | + ... msgid "translator-credits" |
70 | + ... msgstr "%s" |
71 | + ... """ |
72 | + |
73 | + >>> potemplate_content = potemplate_header + po_content % (('',) * 5) |
74 | + >>> pofile_eo_content = pofile_header + po_content % ( |
75 | + ... "baz eo", "Foo eo %s", "Singular eo %s", "Plural eo %s", |
76 | + ... "helpful-eo@example.com") |
77 | + >>> pofile_nl_content = pofile_header + po_content % ( |
78 | + ... "baz nl", "Foo nl %s", "Singular nl %s", "Plural nl %s", |
79 | + ... "helpful-nl@example.com") |
80 | + |
81 | +There is annoying sample data in the queue that needs to be removed. |
82 | + |
83 | + >>> from lp.translations.interfaces.translationimportqueue import ( |
84 | + ... ITranslationImportQueue, RosettaImportStatus) |
85 | + >>> queue = getUtility(ITranslationImportQueue) |
86 | + >>> for entry in queue: |
87 | + ... queue.remove(entry) |
88 | + |
89 | +The files have been uploaded to the queue for a source package and have |
90 | +already been approved. |
91 | + |
92 | + >>> from zope.security.proxy import removeSecurityProxy |
93 | + >>> distroseries = factory.makeUbuntuDistroSeries() |
94 | + >>> naked_distroseries = removeSecurityProxy(distroseries) |
95 | + >>> naked_distroseries.distribution.official_rosetta = True |
96 | + >>> sourcepackagename = factory.makeSourcePackageName() |
97 | + >>> potemplate = factory.makePOTemplate( |
98 | + ... distroseries=distroseries, sourcepackagename=sourcepackagename) |
99 | + >>> pofile_eo = potemplate.newPOFile('eo') |
100 | + >>> pofile_nl = potemplate.newPOFile('nl') |
101 | + |
102 | + >>> from canonical.launchpad.interfaces.launchpad import ( |
103 | + ... ILaunchpadCelebrities) |
104 | + >>> rosetta_experts = getUtility(ILaunchpadCelebrities).rosetta_experts |
105 | + |
106 | + >>> template_entry = queue.addOrUpdateEntry( |
107 | + ... potemplate.path, potemplate_content, True, potemplate.owner, |
108 | + ... distroseries=distroseries, sourcepackagename=sourcepackagename, |
109 | + ... potemplate=potemplate) |
110 | + >>> pofile_eo_entry = queue.addOrUpdateEntry( |
111 | + ... 'eo.po', pofile_eo_content, True, potemplate.owner, |
112 | + ... distroseries=distroseries, sourcepackagename=sourcepackagename, |
113 | + ... potemplate=potemplate, pofile=pofile_eo) |
114 | + >>> pofile_nl_entry = queue.addOrUpdateEntry( |
115 | + ... 'nl.po', pofile_nl_content, True, potemplate.owner, |
116 | + ... distroseries=distroseries, sourcepackagename=sourcepackagename, |
117 | + ... potemplate=potemplate, pofile=pofile_nl) |
118 | + >>> transaction.commit() |
119 | + |
120 | + >>> for entry in queue: |
121 | + ... entry.setStatus(RosettaImportStatus.APPROVED, rosetta_experts) |
122 | + >>> transaction.commit() |
123 | + |
124 | +As it happens, the administrator has blocked imports to the distroseries, e.g. |
125 | +because an in-database update of its translations has been scheduled |
126 | +and we don't want interference from queued imports while that happens. |
127 | +It doesn't really matter whether entries still get auto-approved, but |
128 | +we can't accept new translation imports just now. |
129 | + |
130 | + >>> distroseries.defer_translation_imports |
131 | + True |
132 | + |
133 | + >>> from canonical.launchpad.scripts import FakeLogger |
134 | + >>> from lp.translations.scripts.po_import import TranslationsImport |
135 | + >>> import email |
136 | + >>> from lp.services.mail import stub |
137 | + >>> process = TranslationsImport('poimport', test_args=[]) |
138 | + >>> process.logger = FakeLogger() |
139 | + >>> process.main() |
140 | + DEBUG Starting the import process. |
141 | + INFO No requests pending. |
142 | + |
143 | +When imports are allowed, the import script can do its work. |
144 | + |
145 | + >>> naked_distroseries.defer_translation_imports = False |
146 | + |
147 | + >>> process = TranslationsImport('poimport', test_args=[]) |
148 | + >>> process.logger = FakeLogger() |
149 | + >>> process.main() |
150 | + DEBUG Starting the import process. |
151 | + INFO Importing: Template ... |
152 | + INFO Importing: Esperanto (eo) ... of ... |
153 | + INFO Importing: Dutch (nl) ... of ... |
154 | + INFO Import requests completed. |
155 | + DEBUG Finished the import process. |
156 | + |
157 | +The import script also generates an email similar to the ones we saw |
158 | +composed before, but also sends it. |
159 | + |
160 | + >>> len(stub.test_emails) |
161 | + 1 |
162 | + |
163 | + >>> from_addr, to_addrs, raw_message = stub.test_emails.pop() |
164 | + >>> msg = email.message_from_string(raw_message) |
165 | + >>> print msg["Subject"] |
166 | + Translation template import - ... |
167 | + |
168 | + >>> print msg.get_payload(decode=True) |
169 | + Hello ..., |
170 | + <BLANKLINE> |
171 | + On ..., you uploaded a translation |
172 | + template for ... in Launchpad. |
173 | + <BLANKLINE> |
174 | + The template has now been imported successfully. |
175 | + <BLANKLINE> |
176 | + Thank you, |
177 | + <BLANKLINE> |
178 | + The Launchpad team |
179 | + |
180 | +The entries that remain in the queue as "imported" age over time. |
181 | + |
182 | + >>> import datetime |
183 | + >>> for entry in queue: |
184 | + ... removeSecurityProxy(entry).date_status_changed -= ( |
185 | + ... datetime.timedelta(days=30)) |
186 | + |
187 | + |
188 | +Now the queue gardener runs. This can happen anytime, since it's |
189 | +asynchronous to the po-import script. The script tries to approve any |
190 | +entries that have not been approved, but look like they could be, |
191 | +without human intervention. This involves a bit of guesswork about what |
192 | +the imported file is and where it belongs. It similarly blocks entries |
193 | +that it thinks should be blocked, and also purges deleted or completed |
194 | +entries from the queue. Running at this point, all it does is purge the |
195 | +two hand-approved Welsh translations that have just been imported. |
196 | + |
197 | + >>> import logging |
198 | + >>> from lp.testing.logger import MockLogger |
199 | + >>> from lp.translations.scripts.import_queue_gardener import ( |
200 | + ... ImportQueueGardener) |
201 | + >>> process = ImportQueueGardener('approver', test_args=[]) |
202 | + >>> process.logger = MockLogger() |
203 | + >>> process.logger.setLevel(logging.INFO) |
204 | + >>> process.main() |
205 | + log> Removed 3 entries from the queue. |
206 | + >>> transaction.commit() |
207 | + |
208 | +If users upload two versions of the same file, they are imported in the |
209 | +order in which they were uploaded. |
210 | + |
211 | + >>> import pytz |
212 | + >>> UTC = pytz.timezone('UTC') |
213 | + >>> first_pofile_content = r''' |
214 | + ... msgid "" |
215 | + ... msgstr "" |
216 | + ... "PO-Revision-Date: 2005-06-04 20:41+0100\n" |
217 | + ... "Last-Translator: Foo <no-priv@canonical.com>\n" |
218 | + ... "Content-Type: text/plain; charset=UTF-8\n" |
219 | + ... "X-Rosetta-Export-Date: %s\n" |
220 | + ... |
221 | + ... msgid "Foo %%s" |
222 | + ... msgstr "Bar" |
223 | + ... |
224 | + ... msgid "translator-credits" |
225 | + ... msgstr "The world will never know." |
226 | + ... ''' % datetime.datetime.now(UTC).isoformat() |
227 | + |
228 | + >>> second_pofile_content = r''' |
229 | + ... msgid "" |
230 | + ... msgstr "" |
231 | + ... "PO-Revision-Date: 2005-06-04 21:41+0100\n" |
232 | + ... "Last-Translator: Jordi Mallach <jordi@canonical.com>\n" |
233 | + ... "Content-Type: text/plain; charset=UTF-8\n" |
234 | + ... "X-Rosetta-Export-Date: %s\n" |
235 | + ... |
236 | + ... msgid "Foo %%s" |
237 | + ... msgstr "Bars" |
238 | + ... |
239 | + ... msgid "translator-credits" |
240 | + ... msgstr "I'd like to thank John, Kathy, my pot plants, and all the..." |
241 | + ... ''' % datetime.datetime.now(UTC).isoformat() |
242 | + |
243 | +Attach the first version of the file. |
244 | + |
245 | + >>> entry = queue.addOrUpdateEntry( |
246 | + ... pofile_eo.path, first_pofile_content, False, rosetta_experts, |
247 | + ... sourcepackagename=sourcepackagename, distroseries=distroseries) |
248 | + >>> transaction.commit() |
249 | + |
250 | +It's in the queue now. |
251 | + |
252 | + >>> queue.countEntries() |
253 | + 1 |
254 | + |
255 | +For the second version, we need a new importer. |
256 | + |
257 | + >>> importer_person = factory.makePerson() |
258 | + |
259 | +Attach the second version of the file. |
260 | + |
261 | + >>> entry = queue.addOrUpdateEntry( |
262 | + ... pofile_eo.path, second_pofile_content, False, importer_person, |
263 | + ... sourcepackagename=sourcepackagename, distroseries=distroseries) |
264 | + >>> transaction.commit() |
265 | + |
266 | +It's in the queue now. |
267 | + |
268 | + >>> queue.countEntries() |
269 | + 2 |
270 | + >>> print entry.status.name |
271 | + NEEDS_REVIEW |
272 | + |
273 | +The queue gardener runs again. This time it sees the two submitted |
274 | +translations and approves them for import based on some heuristic |
275 | +intelligence. |
276 | + |
277 | + >>> process = ImportQueueGardener('approver', test_args=[]) |
278 | + >>> process.logger = MockLogger() |
279 | + >>> process.logger.setLevel(logging.INFO) |
280 | + >>> process.main() |
281 | + log> The automatic approval system approved some entries. |
282 | + >>> print entry.status.name |
283 | + APPROVED |
284 | + >>> from canonical.launchpad.ftests import syncUpdate |
285 | + >>> syncUpdate(entry) |
286 | + |
287 | +Now that these submissions have been approved, the next run of the |
288 | +import script picks them up and processes them. |
289 | + |
290 | + >>> process = TranslationsImport('poimport', test_args=[]) |
291 | + >>> process.logger = FakeLogger() |
292 | + >>> process.main() |
293 | + DEBUG Starting the import process. |
294 | + INFO Importing: Esperanto (eo) ... of ... |
295 | + INFO Importing: Esperanto (eo) ... of ... |
296 | + INFO Import requests completed. |
297 | + DEBUG Finished the import process. |
298 | + |
299 | + >>> print entry.status.name |
300 | + IMPORTED |
301 | + >>> syncUpdate(entry) |
302 | + |
303 | +And there are no more entries to import |
304 | + |
305 | + >>> queue.getFirstEntryToImport() is None |
306 | + True |
307 | + |
308 | +We've imported a new translation for "Foo %s." |
309 | + |
310 | + >>> from lp.services.worlddata.interfaces.language import ILanguageSet |
311 | + >>> esperanto = getUtility(ILanguageSet).getLanguageByCode('eo') |
312 | + >>> foos = potemplate['Foo %s'].getLocalTranslationMessages( |
313 | + ... potemplate, esperanto) |
314 | + >>> sorted([foo.msgstr0.translation for foo in foos]) |
315 | + [u'Bar', u'Bars'] |
316 | + |
317 | +Since this last upload was not the upstream one, however, its credits |
318 | +message translations were ignored. |
319 | + |
320 | + >>> potmsgset = pofile_eo.potemplate.getPOTMsgSetByMsgIDText( |
321 | + ... u'translator-credits') |
322 | + >>> message = potmsgset.getCurrentTranslationMessage( |
323 | + ... pofile_eo.potemplate, pofile_eo.language) |
324 | + >>> message.msgstr0.translation |
325 | + u'helpful-eo@example.com' |
326 | + >>> list(potemplate['translator-credits'].getLocalTranslationMessages( |
327 | + ... potemplate, esperanto)) |
328 | + [] |
329 | + |
330 | + |
331 | +No Contact Address |
332 | +------------------ |
333 | + |
334 | +Not every user has a valid email address. For instance, Kermit the |
335 | +Hermit has none at the moment. |
336 | + |
337 | + >>> from canonical.launchpad.interfaces.emailaddress import ( |
338 | + ... EmailAddressStatus) |
339 | + >>> from canonical.launchpad.helpers import get_contact_email_addresses |
340 | + >>> hermit = factory.makePerson( |
341 | + ... name='hermit', email_address_status=EmailAddressStatus.OLD) |
342 | + |
343 | + >>> len(get_contact_email_addresses(hermit)) |
344 | + 0 |
345 | + |
346 | +Kermit uploads a translation, which gets approved. |
347 | + |
348 | + >>> pofile = factory.makePOFile('lo', potemplate) |
349 | + >>> entry = queue.addOrUpdateEntry( |
350 | + ... 'lo.po', 'Invalid content', True, hermit, |
351 | + ... pofile=pofile, potemplate=potemplate, |
352 | + ... distroseries=distroseries, sourcepackagename=sourcepackagename) |
353 | + >>> entry.setStatus(RosettaImportStatus.APPROVED, rosetta_experts) |
354 | + >>> transaction.commit() |
355 | + |
356 | +The import fails. The importer would like to send Kermit an email about |
357 | +this, but is unable to. This is unfortunate, but does not faze the |
358 | +importer. It completes normally. |
359 | + |
360 | + >>> process = TranslationsImport('poimport', test_args=[]) |
361 | + >>> process.logger = FakeLogger() |
362 | + >>> process.main() |
363 | + DEBUG Starting the import process. |
364 | + INFO Importing: Lao ... |
365 | + INFO Import requests completed. |
366 | + DEBUG Finished the import process. |
367 | + |
368 | + >>> print entry.status.name |
369 | + FAILED |
370 | |
371 | === modified file 'lib/lp/translations/doc/poimport.txt' |
372 | --- lib/lp/translations/doc/poimport.txt 2010-08-10 14:39:46 +0000 |
373 | +++ lib/lp/translations/doc/poimport.txt 2010-09-21 16:23:42 +0000 |
374 | @@ -1,23 +1,20 @@ |
375 | -= PO Imports = |
376 | +========== |
377 | +PO Imports |
378 | +========== |
379 | |
380 | The tale of a PO template and a PO file and how they get imported into |
381 | Rosetta. |
382 | |
383 | - |
384 | -== Test Setup == |
385 | +Test Setup |
386 | +========== |
387 | |
388 | Here are some imports we need to get this test running. |
389 | |
390 | - >>> from canonical.launchpad.ftests import syncUpdate |
391 | - >>> from canonical.launchpad.interfaces import ( |
392 | - ... ILanguageSet, ILaunchpadCelebrities, IPersonSet, IProductSet) |
393 | + >>> from canonical.launchpad.interfaces.launchpad import ( |
394 | + ... ILaunchpadCelebrities) |
395 | + >>> from lp.registry.interfaces.person import IPersonSet |
396 | >>> from lp.translations.interfaces.translationimportqueue import ( |
397 | ... ITranslationImportQueue, RosettaImportStatus) |
398 | - >>> from lp.registry.model.sourcepackagename import SourcePackageName |
399 | - >>> from lp.translations.model.potemplate import POTemplateSubset |
400 | - >>> from lp.translations.scripts.po_import import TranslationsImport |
401 | - >>> from lp.translations.scripts.import_queue_gardener import ( |
402 | - ... ImportQueueGardener) |
403 | >>> import datetime |
404 | >>> import pytz |
405 | >>> UTC = pytz.timezone('UTC') |
406 | @@ -36,18 +33,15 @@ |
407 | >>> login('carlos@canonical.com') |
408 | |
409 | |
410 | -== Importing a Template == |
411 | +Importing a Template |
412 | +==================== |
413 | |
414 | Normal procedure is to import a template, followed by translations. |
415 | A template is created first. After that, imports are done using the |
416 | POFile.importFromQueue and POTemplate.importFromQueue methods. |
417 | |
418 | - >>> from lp.registry.model.productrelease import ProductRelease |
419 | - >>> release = ProductRelease.get(3) |
420 | - >>> release.productseries.product.name |
421 | - u'firefox' |
422 | - >>> series = release.productseries |
423 | - >>> subset = POTemplateSubset(productseries=series) |
424 | + >>> distroseries = factory.makeUbuntuDistroSeries() |
425 | + >>> sourcepackagename = factory.makeSourcePackageName() |
426 | |
427 | Here's the person who'll be doing the import. |
428 | |
429 | @@ -56,10 +50,8 @@ |
430 | |
431 | And this is the POTemplate where the import will be done. |
432 | |
433 | - >>> potemplate = subset.new( |
434 | - ... name='firefox', |
435 | - ... translation_domain='firefox', |
436 | - ... path='po/firefox.pot', |
437 | + >>> potemplate = factory.makePOTemplate( |
438 | + ... distroseries=distroseries, sourcepackagename=sourcepackagename, |
439 | ... owner=person) |
440 | >>> potemplate_id = potemplate.id |
441 | |
442 | @@ -110,7 +102,8 @@ |
443 | >>> translation_import_queue = getUtility(ITranslationImportQueue) |
444 | >>> entry = translation_import_queue.addOrUpdateEntry( |
445 | ... potemplate.path, potemplate_contents, True, potemplate.owner, |
446 | - ... productseries=series, potemplate=potemplate) |
447 | + ... distroseries=distroseries, sourcepackagename=sourcepackagename, |
448 | + ... potemplate=potemplate) |
449 | |
450 | The file data is stored in the Librarian, so we have to commit the |
451 | transaction to make sure it's stored properly. |
452 | @@ -143,13 +136,13 @@ |
453 | |
454 | A successful import is confirmed by email. |
455 | |
456 | - >>> subject |
457 | - u'Translation template import - firefox in Mozilla Firefox trunk' |
458 | + >>> print subject |
459 | + Translation template import - ... |
460 | >>> print body |
461 | Hello Mark Shuttleworth, |
462 | <BLANKLINE> |
463 | On ..., you uploaded a translation |
464 | - template for firefox in Mozilla Firefox trunk in Launchpad. |
465 | + template for ... in Launchpad. |
466 | <BLANKLINE> |
467 | The template has now been imported successfully. |
468 | <BLANKLINE> |
469 | @@ -174,7 +167,8 @@ |
470 | u'test.c:13' |
471 | |
472 | |
473 | -=== Import Preconditions === |
474 | +Import Preconditions |
475 | +==================== |
476 | |
477 | The API for POTemplate.importFromQueue demands a translation import |
478 | queue entry to import. |
479 | @@ -195,12 +189,7 @@ |
480 | any other file would be an error. |
481 | |
482 | >>> entry.setStatus(RosettaImportStatus.APPROVED, rosetta_experts) |
483 | - >>> from lp.translations.interfaces.potemplate import IPOTemplateSet |
484 | - >>> other_product = getUtility(IProductSet).getByName('netapplet') |
485 | - >>> other_productseries = other_product.getSeries('trunk') |
486 | - >>> template_set = getUtility(IPOTemplateSet) |
487 | - >>> other_template = template_set.getPOTemplateByPathAndOrigin( |
488 | - ... 'po/netapplet.pot', productseries=other_productseries) |
489 | + >>> other_template = factory.makePOTemplate() |
490 | >>> other_template.importFromQueue(entry) |
491 | Traceback (most recent call last): |
492 | ... |
493 | @@ -208,7 +197,8 @@ |
494 | to. |
495 | |
496 | |
497 | -== Importing a Translation == |
498 | +Importing a Translation |
499 | +======================= |
500 | |
501 | Now let's get a PO file to import. |
502 | |
503 | @@ -217,8 +207,8 @@ |
504 | |
505 | By default, we got a safe path to prevent collisions with other IPOFile. |
506 | |
507 | - >>> pofile.path |
508 | - u'po/firefox-cy.po' |
509 | + >>> print pofile.path |
510 | + generic-string...-cy.po |
511 | |
512 | Let's override the default good path with one we know is the right one. |
513 | |
514 | @@ -236,7 +226,8 @@ |
515 | 1 |
516 | |
517 | |
518 | -=== Import With Errors === |
519 | +Import With Errors |
520 | +------------------ |
521 | |
522 | Here are the contents of the file we'll be importing. It has some |
523 | validation errors. |
524 | @@ -285,7 +276,8 @@ |
525 | |
526 | >>> entry = translation_import_queue.addOrUpdateEntry( |
527 | ... pofile.path, pofile_with_errors, True, person, |
528 | - ... productseries=series, potemplate=potemplate) |
529 | + ... distroseries=distroseries, sourcepackagename=sourcepackagename, |
530 | + ... potemplate=potemplate) |
531 | >>> transaction.commit() |
532 | |
533 | The guess IPOFile should be the same we already had. |
534 | @@ -381,7 +373,7 @@ |
535 | Hello Mark Shuttleworth, |
536 | <BLANKLINE> |
537 | On ..., you uploaded 5 |
538 | - Welsh (cy) translations for firefox in Mozilla Firefox trunk in Launchpad. |
539 | + Welsh (cy) translations for ... in Launchpad. |
540 | <BLANKLINE> |
541 | There were problems with 1 of these translations. |
542 | <BLANKLINE> |
543 | @@ -410,7 +402,8 @@ |
544 | msgstr "blah %i" |
545 | |
546 | |
547 | -=== Import With Warnings === |
548 | +Import With Warnings |
549 | +-------------------- |
550 | |
551 | The import may also succeed but produce syntax warnings. These need not |
552 | be tied to particular messages (they could be in the header, for |
553 | @@ -435,14 +428,14 @@ |
554 | ... msgid "a" |
555 | ... msgstr "b" |
556 | ... ''' % datetime.datetime.now(UTC).isoformat() |
557 | - >>> sumerian_pofile = potemplate.newPOFile('sux') |
558 | + >>> eo_pofile = potemplate.newPOFile('eo') |
559 | >>> warning_entry = translation_import_queue.addOrUpdateEntry( |
560 | - ... 'sux.po', pofile_with_warning, False, potemplate.owner, |
561 | - ... productseries=series, potemplate=potemplate, |
562 | - ... pofile=sumerian_pofile) |
563 | + ... 'eo.po', pofile_with_warning, False, potemplate.owner, |
564 | + ... distroseries=distroseries, sourcepackagename=sourcepackagename, |
565 | + ... potemplate=potemplate, pofile=eo_pofile) |
566 | >>> transaction.commit() |
567 | >>> warning_entry.setStatus(RosettaImportStatus.APPROVED, rosetta_experts) |
568 | - >>> (subject, message) = sumerian_pofile.importFromQueue(warning_entry) |
569 | + >>> (subject, message) = eo_pofile.importFromQueue(warning_entry) |
570 | |
571 | The warning is noted in the confirmation email. Note that this |
572 | particular warning condition is recognized fairly late, so the line |
573 | @@ -473,7 +466,8 @@ |
574 | >>> warning_entry.setStatus(RosettaImportStatus.DELETED, rosetta_experts) |
575 | |
576 | |
577 | -=== Import Without Errors === |
578 | +Import Without Errors |
579 | +--------------------- |
580 | |
581 | Now, let's import one without errors. |
582 | |
583 | @@ -495,7 +489,8 @@ |
584 | ... ''' % datetime.datetime.now(UTC).isoformat() |
585 | >>> entry = translation_import_queue.addOrUpdateEntry( |
586 | ... pofile.path, pofile_without_errors, True, rosetta_experts, |
587 | - ... productseries=series, potemplate=potemplate) |
588 | + ... distroseries=distroseries, sourcepackagename=sourcepackagename, |
589 | + ... potemplate=potemplate) |
590 | >>> transaction.commit() |
591 | |
592 | The new upload clears the entry's error_output. |
593 | @@ -580,7 +575,8 @@ |
594 | u'helpful@example.com' |
595 | |
596 | |
597 | -=== Import Preconditions === |
598 | +Import Preconditions |
599 | +==================== |
600 | |
601 | The API for POFile.importFromQueue demands a translation import queue |
602 | entry to import. |
603 | @@ -617,296 +613,28 @@ |
604 | to. |
605 | |
606 | |
607 | -== Cron Scripts == |
608 | - |
609 | -We tested already that the functionality works. Now it's time to know |
610 | -if the cronscript has any problem. |
611 | - |
612 | -First, we are going to reactivate the entries that were already |
613 | -imported or failed. Note that we'll only reactivate the entries we use |
614 | -in this test; We don't touch entries that were in the queue previously. |
615 | - |
616 | - >>> for entry in translation_import_queue: |
617 | - ... if (entry.status == RosettaImportStatus.IMPORTED or |
618 | - ... entry.status == RosettaImportStatus.FAILED) and ( |
619 | - ... entry.productseries == series): |
620 | - ... entry.setStatus(RosettaImportStatus.APPROVED, rosetta_experts) |
621 | - ... syncUpdate(entry) |
622 | - >>> transaction.commit() |
623 | - |
624 | -And run the import script. |
625 | - |
626 | - >>> import email |
627 | - >>> from lp.services.mail import stub |
628 | - >>> process = TranslationsImport('poimport', test_args=[]) |
629 | - >>> process.logger = FakeLogger() |
630 | - >>> process.main() |
631 | - DEBUG Starting the import process. |
632 | - INFO Importing: Template "firefox" in Mozilla Firefox trunk |
633 | - INFO Importing: Welsh (cy) ... of firefox in Mozilla Firefox trunk |
634 | - INFO Importing: Welsh (cy) ... of firefox in Mozilla Firefox trunk |
635 | - INFO Import requests completed. |
636 | - DEBUG Finished the import process. |
637 | - |
638 | -The import script also generates an email similar to the ones we saw |
639 | -composed before, but also sends it. |
640 | - |
641 | - >>> len(stub.test_emails) |
642 | - 1 |
643 | - |
644 | - >>> from_addr, to_addrs, raw_message = stub.test_emails.pop() |
645 | - >>> msg = email.message_from_string(raw_message) |
646 | - >>> msg["Subject"] |
647 | - 'Translation template import - firefox in Mozilla Firefox trunk' |
648 | - |
649 | - >>> print msg.get_payload(decode=True) |
650 | - Hello Mark Shuttleworth, |
651 | - <BLANKLINE> |
652 | - On ..., you uploaded a translation |
653 | - template for firefox in Mozilla Firefox trunk in Launchpad. |
654 | - <BLANKLINE> |
655 | - The template has now been imported successfully. |
656 | - <BLANKLINE> |
657 | - Thank you, |
658 | - <BLANKLINE> |
659 | - The Launchpad team |
660 | - |
661 | -Now the queue gardener runs. This can happen anytime, since it's |
662 | -asynchronous to the po-import script. The script tries to approve any |
663 | -entries that have not been approved, but look like they could be, |
664 | -without human intervention. This involves a bit of guesswork about what |
665 | -the imported file is and where it belongs. It similarly blocks entries |
666 | -that it thinks should be blocked, and also purges deleted or completed |
667 | -entries from the queue. Running at this point, all it does is purge the |
668 | -two hand-approved Welsh translations that have just been imported. |
669 | - |
670 | - >>> import logging |
671 | - >>> from lp.testing.logger import MockLogger |
672 | - >>> process = ImportQueueGardener('approver', test_args=[]) |
673 | - >>> process.logger = MockLogger() |
674 | - >>> process.logger.setLevel(logging.INFO) |
675 | - >>> process.main() |
676 | - log> Removed 2 entries from the queue. |
677 | - >>> transaction.commit() |
678 | - |
679 | -If users upload two versions of the same file, they are imported in the |
680 | -order in which they were uploaded. |
681 | - |
682 | - >>> first_pofile_content = r''' |
683 | - ... msgid "" |
684 | - ... msgstr "" |
685 | - ... "PO-Revision-Date: 2005-06-04 20:41+0100\n" |
686 | - ... "Last-Translator: Foo <no-priv@canonical.com>\n" |
687 | - ... "Content-Type: text/plain; charset=UTF-8\n" |
688 | - ... "X-Rosetta-Export-Date: %s\n" |
689 | - ... |
690 | - ... msgid "Foo %%s" |
691 | - ... msgstr "Bar" |
692 | - ... |
693 | - ... msgid "translator-credits" |
694 | - ... msgstr "The world will never know." |
695 | - ... ''' % datetime.datetime.now(UTC).isoformat() |
696 | - |
697 | - >>> second_pofile_content = r''' |
698 | - ... msgid "" |
699 | - ... msgstr "" |
700 | - ... "PO-Revision-Date: 2005-06-04 21:41+0100\n" |
701 | - ... "Last-Translator: Jordi Mallach <jordi@canonical.com>\n" |
702 | - ... "Content-Type: text/plain; charset=UTF-8\n" |
703 | - ... "X-Rosetta-Export-Date: %s\n" |
704 | - ... |
705 | - ... msgid "Foo %%s" |
706 | - ... msgstr "Bars" |
707 | - ... |
708 | - ... msgid "translator-credits" |
709 | - ... msgstr "I'd like to thank John, Kathy, my pot plants, and all the..." |
710 | - ... ''' % datetime.datetime.now(UTC).isoformat() |
711 | - |
712 | -We flush the entry contents. |
713 | - |
714 | - >>> for entry in translation_import_queue: |
715 | - ... translation_import_queue.remove(entry) |
716 | - >>> translation_import_queue.countEntries() |
717 | - 0 |
718 | - |
719 | -Attach the first version of the file. |
720 | - |
721 | - >>> entry = translation_import_queue.addOrUpdateEntry( |
722 | - ... pofile.path, first_pofile_content, False, rosetta_experts, |
723 | - ... sourcepackagename=pofile.potemplate.sourcepackagename, |
724 | - ... distroseries=pofile.potemplate.distroseries, |
725 | - ... productseries=pofile.potemplate.productseries) |
726 | - >>> transaction.commit() |
727 | - |
728 | -It's in the queue now. |
729 | - |
730 | - >>> translation_import_queue.countEntries() |
731 | - 1 |
732 | - |
733 | -For the second version, we need a new importer, in this case, Jordi. |
734 | - |
735 | - >>> jordi = person_set.getByName('jordi') |
736 | - |
737 | -Attach the second version of the file. |
738 | - |
739 | - >>> entry = translation_import_queue.addOrUpdateEntry( |
740 | - ... pofile.path, second_pofile_content, False, jordi, |
741 | - ... sourcepackagename=pofile.potemplate.sourcepackagename, |
742 | - ... distroseries=pofile.potemplate.distroseries, |
743 | - ... productseries=pofile.potemplate.productseries) |
744 | - >>> transaction.commit() |
745 | - |
746 | -It's in the queue now. |
747 | - |
748 | - >>> translation_import_queue.countEntries() |
749 | - 2 |
750 | - >>> print entry.status.name |
751 | - NEEDS_REVIEW |
752 | - |
753 | -The queue gardener runs again. This time it sees the two submitted |
754 | -translations and approves them for import based on some heuristic |
755 | -intelligence. |
756 | - |
757 | - >>> process = ImportQueueGardener('approver', test_args=[]) |
758 | - >>> process.logger = MockLogger() |
759 | - >>> process.logger.setLevel(logging.INFO) |
760 | - >>> process.main() |
761 | - log> The automatic approval system approved some entries. |
762 | - >>> print entry.status.name |
763 | - APPROVED |
764 | - >>> syncUpdate(entry) |
765 | - |
766 | -Now that these submissions have been approved, the next run of the |
767 | -import script picks them up and processes them. |
768 | - |
769 | - >>> process = TranslationsImport('poimport', test_args=[]) |
770 | - >>> process.logger = FakeLogger() |
771 | - >>> process.main() |
772 | - DEBUG Starting the import process. |
773 | - INFO Importing: Welsh (cy) ... of firefox in Mozilla Firefox trunk |
774 | - INFO Importing: Welsh (cy) ... of firefox in Mozilla Firefox trunk |
775 | - INFO Import requests completed. |
776 | - DEBUG Finished the import process. |
777 | - |
778 | - >>> print entry.status.name |
779 | - IMPORTED |
780 | - >>> syncUpdate(entry) |
781 | - |
782 | -And there are no more entries to import |
783 | - |
784 | - >>> translation_import_queue.getFirstEntryToImport() is None |
785 | - True |
786 | - |
787 | -We've imported a new translation for "Foo %s." |
788 | - |
789 | - >>> welsh = getUtility(ILanguageSet).getLanguageByCode('cy') |
790 | - >>> foos = potemplate['Foo %s'].getLocalTranslationMessages( |
791 | - ... potemplate, welsh) |
792 | - >>> sorted([foo.msgstr0.translation for foo in foos]) |
793 | - [u'Bar', u'Bars', u'blah %i'] |
794 | - |
795 | -Since this last upload was not the upstream one, however, its credits |
796 | -message translations were ignored. |
797 | - |
798 | - >>> message = get_pofile_translation_message( |
799 | - ... pofile, u'translator-credits') |
800 | - >>> message.msgstr0.translation |
801 | - u'helpful@example.com' |
802 | - >>> list(potemplate['translator-credits'].getLocalTranslationMessages( |
803 | - ... potemplate, welsh)) |
804 | - [] |
805 | - |
806 | -Imports so far have been associated with a product series. We can also |
807 | -submit translations for a distroseries. |
808 | - |
809 | - >>> from lp.registry.interfaces.distribution import ( |
810 | - ... IDistributionSet) |
811 | - >>> ubuntu = getUtility(IDistributionSet).getByName('ubuntu') |
812 | - >>> warty = ubuntu.getSeries('warty') |
813 | - >>> print warty.name |
814 | - warty |
815 | - >>> firefox_name = SourcePackageName.byName('mozilla-firefox') |
816 | - >>> subset = POTemplateSubset(sourcepackagename=firefox_name, |
817 | - ... distroseries=warty) |
818 | - >>> potemplate = subset.new( |
819 | - ... name='firefox-warty', |
820 | - ... translation_domain='firefox-warty', |
821 | - ... path='po/firefox.pot', |
822 | - ... owner=person) |
823 | - |
824 | -As it happens, the administrator has blocked imports to warty, e.g. |
825 | -because an in-database update of its translations has been scheduled |
826 | -and we don't want interference from queued imports while that happens. |
827 | -It doesn't really matter whether entries still get auto-approved, but |
828 | -we can't accept new translation imports just now. |
829 | - |
830 | - >>> warty.defer_translation_imports = True |
831 | - >>> syncUpdate(warty) |
832 | - |
833 | -Nevertheless, someone submits an import request for warty, not knowing |
834 | -or caring that imports are deferred. The entry still gets approved as |
835 | -normal: |
836 | - |
837 | - >>> entry = translation_import_queue.addOrUpdateEntry( |
838 | - ... potemplate.path, potemplate_contents, True, potemplate.owner, |
839 | - ... sourcepackagename=firefox_name, distroseries=warty, |
840 | - ... potemplate=potemplate) |
841 | - >>> entry.setStatus(RosettaImportStatus.APPROVED, rosetta_experts) |
842 | - >>> syncUpdate(entry) |
843 | - >>> transaction.commit() |
844 | - |
845 | -Since imports for warty are suspended, and the only entry we happen to |
846 | -have waiting right now is for warty, the queue has no importable |
847 | -entries for us. |
848 | - |
849 | - >>> warty.getFirstEntryToImport() is None |
850 | - True |
851 | - |
852 | -So if we try to import now, nothing happens. Our request remains on the |
853 | -queue, but doesn't become a candidate for processing until warty |
854 | -imports are resumed. |
855 | - |
856 | - >>> process = TranslationsImport('poimport', test_args=[]) |
857 | - >>> process.logger = FakeLogger() |
858 | - >>> process.main() |
859 | - DEBUG Starting the import process. |
860 | - INFO No requests pending. |
861 | - |
862 | - >>> print entry.status.name |
863 | - APPROVED |
864 | - |
865 | -Once imports are allowed again, the import is done after all. |
866 | - |
867 | - >>> warty.defer_translation_imports = False |
868 | - >>> syncUpdate(warty) |
869 | - >>> (subject, body) = potemplate.importFromQueue(entry, FakeLogger()) |
870 | - |
871 | - >>> print entry.status.name |
872 | - IMPORTED |
873 | - |
874 | - |
875 | -== Plural forms handling == |
876 | +Plural forms handling |
877 | +===================== |
878 | |
879 | Apart from the basic plural form handling, which is documented above as |
880 | part of the import process, there are some peculiarities with importing |
881 | plural forms we want documented as well. |
882 | |
883 | -For a language such as Divehi, which has no plural forms defined, we |
884 | +For a language that has no plural forms defined, we |
885 | default to two plural forms (the most common value for the number of |
886 | plural forms). |
887 | |
888 | - >>> divehi = getUtility(ILanguageSet)['dv'] |
889 | - >>> print divehi.pluralforms |
890 | + >>> language = factory.makeLanguage() |
891 | + >>> print language.pluralforms |
892 | None |
893 | |
894 | - >>> firefox = getUtility(IProductSet).getByName('firefox') |
895 | - >>> firefox_trunk = firefox.getSeries('trunk') |
896 | - >>> firefox_potemplate = firefox_trunk.getPOTemplate('firefox') |
897 | - >>> firefox_dv = firefox_potemplate.newPOFile(divehi.code) |
898 | - >>> firefox_dv.plural_forms |
899 | + >>> potemplate = factory.makePOTemplate( |
900 | + ... distroseries=distroseries, sourcepackagename=sourcepackagename) |
901 | + >>> pofile = potemplate.newPOFile(language.code) |
902 | + >>> pofile.plural_forms |
903 | 2 |
904 | |
905 | -We'll import a POFile with 3 plural forms into Divehi POFile: |
906 | +We'll import a POFile with 3 plural forms into this POFile: |
907 | |
908 | >>> pofile_with_plurals = r''' |
909 | ... msgid "" |
910 | @@ -925,16 +653,18 @@ |
911 | ... msgstr[2] "Third form %%d" |
912 | ... ''' % datetime.datetime.now(UTC).isoformat() |
913 | |
914 | -We now import this POFile as Divehi translation of Firefox trunk: |
915 | +We now import this POFile as this language's translation for the soure |
916 | +package: |
917 | |
918 | >>> entry = translation_import_queue.addOrUpdateEntry( |
919 | - ... firefox_dv.path, pofile_with_plurals, True, person, |
920 | - ... productseries=firefox_trunk, potemplate=firefox_potemplate) |
921 | + ... pofile.path, pofile_with_plurals, True, person, |
922 | + ... distroseries=distroseries, sourcepackagename=sourcepackagename, |
923 | + ... potemplate=potemplate) |
924 | >>> # Allow Librarian to see the change. |
925 | >>> transaction.commit() |
926 | - >>> entry.pofile = firefox_dv |
927 | + >>> entry.pofile = pofile |
928 | >>> entry.setStatus(RosettaImportStatus.APPROVED, rosetta_experts) |
929 | - >>> (subject, body) = firefox_dv.importFromQueue(entry, FakeLogger()) |
930 | + >>> (subject, body) = pofile.importFromQueue(entry, FakeLogger()) |
931 | >>> flush_database_updates() |
932 | >>> print entry.status.name |
933 | IMPORTED |
934 | @@ -943,21 +673,22 @@ |
935 | translations (which is a default when the language has no plural forms |
936 | specified): |
937 | |
938 | - >>> potmsgset_plural = firefox_potemplate.getPOTMsgSetByMsgIDText( |
939 | + >>> potmsgset_plural = potemplate.getPOTMsgSetByMsgIDText( |
940 | ... u'Singular %d', u'Plural %d') |
941 | - >>> current_dv = potmsgset_plural.getCurrentTranslationMessage( |
942 | - ... firefox_potemplate, divehi) |
943 | - >>> current_dv.translations |
944 | + >>> current = potmsgset_plural.getCurrentTranslationMessage( |
945 | + ... potemplate, language) |
946 | + >>> current.translations |
947 | [u'First form %d', u'Second form %d'] |
948 | |
949 | However, even the third form will be imported into database (this is |
950 | useful for when we finally define the number of plural forms for the |
951 | language, we should not have to reimport all translations): |
952 | |
953 | - >>> current_dv.msgstr2.translation |
954 | + >>> current.msgstr2.translation |
955 | u'Third form %d' |
956 | |
957 | -== Upstream import notifications == |
958 | +Upstream import notifications |
959 | +============================= |
960 | |
961 | Add an upstream POFile import (i.e. from a package or bzr branch), |
962 | approve and import it. |
963 | @@ -974,12 +705,12 @@ |
964 | ... msgid "foo" |
965 | ... msgstr "blah" |
966 | ... ''' |
967 | - >>> pofile = factory.makePOFile('sr') |
968 | + >>> pofile = factory.makePOFile('sr', potemplate=potemplate) |
969 | >>> from_upstream = True |
970 | >>> entry = translation_import_queue.addOrUpdateEntry( |
971 | ... pofile.path, pofile_contents, from_upstream, person, |
972 | - ... productseries=pofile.potemplate.productseries, |
973 | - ... potemplate=pofile.potemplate, pofile=pofile) |
974 | + ... distroseries=distroseries, sourcepackagename=sourcepackagename, |
975 | + ... potemplate=potemplate, pofile=pofile) |
976 | >>> transaction.commit() |
977 | >>> entry.setStatus(RosettaImportStatus.APPROVED, rosetta_experts) |
978 | >>> (subject, message) = pofile.importFromQueue(entry) |
979 | @@ -997,8 +728,8 @@ |
980 | >>> pofile_contents = pofile_contents[:-2] |
981 | >>> entry = translation_import_queue.addOrUpdateEntry( |
982 | ... pofile.path, pofile_contents, from_upstream, person, |
983 | - ... productseries=pofile.potemplate.productseries, |
984 | - ... potemplate=pofile.potemplate, pofile=pofile) |
985 | + ... distroseries=distroseries, sourcepackagename=sourcepackagename, |
986 | + ... potemplate=potemplate, pofile=pofile) |
987 | >>> transaction.commit() |
988 | >>> entry.setStatus(RosettaImportStatus.APPROVED, rosetta_experts) |
989 | >>> (subject, message) = pofile.importFromQueue(entry) |
990 | @@ -1010,44 +741,3 @@ |
991 | >>> subject |
992 | u'Import problem - Serbian (sr) - ...' |
993 | |
994 | - |
995 | -No Contact Address |
996 | ------------------- |
997 | - |
998 | -Not every user has a valid email address. For instance, Kermit the |
999 | -Hermit has none at the moment. |
1000 | - |
1001 | - >>> from canonical.launchpad.interfaces.emailaddress import ( |
1002 | - ... EmailAddressStatus) |
1003 | - >>> from canonical.launchpad.helpers import get_contact_email_addresses |
1004 | - >>> hermit = factory.makePerson( |
1005 | - ... name='hermit', email_address_status=EmailAddressStatus.OLD) |
1006 | - |
1007 | - >>> len(get_contact_email_addresses(hermit)) |
1008 | - 0 |
1009 | - |
1010 | -Kermit uploads a translation, which gets approved. |
1011 | - |
1012 | - >>> pofile = factory.makePOFile('lo') |
1013 | - |
1014 | - >>> entry = translation_import_queue.addOrUpdateEntry( |
1015 | - ... 'lo.po', 'Invalid content', True, hermit, |
1016 | - ... pofile=pofile, potemplate=pofile.potemplate, |
1017 | - ... productseries=pofile.potemplate.productseries) |
1018 | - >>> entry.setStatus(RosettaImportStatus.APPROVED, rosetta_experts) |
1019 | - >>> transaction.commit() |
1020 | - |
1021 | -The import fails. The importer would like to send Kermit an email about |
1022 | -this, but is unable to. This is unfortunate, but does not faze the |
1023 | -importer. It completes normally. |
1024 | - |
1025 | - >>> process = TranslationsImport('poimport', test_args=[]) |
1026 | - >>> process.logger = FakeLogger() |
1027 | - >>> process.main() |
1028 | - DEBUG Starting the import process. |
1029 | - INFO Importing: Lao ... |
1030 | - INFO Import requests completed. |
1031 | - DEBUG Finished the import process. |
1032 | - |
1033 | - >>> print entry.status.name |
1034 | - FAILED |
Hi Henning,
This changes looks good. r=mars
Maris