Merge lp:~henninge/launchpad/recife-poimport into lp:~launchpad/launchpad/recife

Proposed by Henning Eggers
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
Reviewer Review Type Date Requested Status
Māris Fogels (community) Approve
Review via email: mp+36165@code.launchpad.net

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_upstream". Using a source package instead of a product series avoids collisions between old and new model.

- 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-script.txt. This new file is completely independent of sample data, it even clears out the sample data from the queue before starting.

Since the new model is explicitly referring to Ubuntu in many places, I found it useful to have a "makeUbuntuDistroSeries" factory methods. This avoids having to deal with Celebrities in the test.

== Test ==

bin/test -vvcm lp.translations -t poimport.txt -t poimport-script.txt

To post a comment you must log in.
Revision history for this message
Māris Fogels (mars) wrote :

Hi Henning,

This changes looks good. r=mars

Maris

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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

Subscribers

People subscribed via source and target branches