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
=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py 2010-09-08 02:19:20 +0000
+++ lib/lp/testing/factory.py 2010-09-21 16:23:42 +0000
@@ -1848,8 +1848,18 @@
1848 series.status = status1848 series.status = status
1849 return ProxyFactory(series)1849 return ProxyFactory(series)
18501850
1851 def makeUbuntuDistroRelease(self, version=None,
1852 status=SeriesStatus.DEVELOPMENT,
1853 parent_series=None, name=None,
1854 displayname=None):
1855 """Short cut to use the celebrity 'ubuntu' as the distribution."""
1856 ubuntu = getUtility(ILaunchpadCelebrities).ubuntu
1857 return self.makeDistroRelease(
1858 ubuntu, version, status, parent_series, name, displayname)
1859
1851 # Most people think of distro releases as distro series.1860 # Most people think of distro releases as distro series.
1852 makeDistroSeries = makeDistroRelease1861 makeDistroSeries = makeDistroRelease
1862 makeUbuntuDistroSeries = makeUbuntuDistroRelease
18531863
1854 def makeDistroSeriesDifference(1864 def makeDistroSeriesDifference(
1855 self, derived_series=None, source_package_name_str=None,1865 self, derived_series=None, source_package_name_str=None,
18561866
=== added file 'lib/lp/translations/doc/poimport-script.txt'
--- lib/lp/translations/doc/poimport-script.txt 1970-01-01 00:00:00 +0000
+++ lib/lp/translations/doc/poimport-script.txt 2010-09-21 16:23:42 +0000
@@ -0,0 +1,342 @@
1Import Script
2=============
3
4The imports are performed by a dedicated cron script.
5
6A template and two pofile will be imported.
7
8 >>> potemplate_header = r"""
9 ... msgid ""
10 ... msgstr ""
11 ... "POT-Creation-Date: 2004-07-11 16:16+0900\n"
12 ... "Content-Type: text/plain; charset=CHARSET\n"
13 ... "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
14 ...
15 ... """
16
17 >>> pofile_header = r"""
18 ... msgid ""
19 ... msgstr ""
20 ... "PO-Revision-Date: 2005-06-03 20:41+0100\n"
21 ... "Last-Translator: Foo <no-priv@canonical.com>\n"
22 ... "Content-Type: text/plain; charset=UTF-8\n"
23 ... "Plural-Forms: nplurals=2; plural=(n!=1);\n"
24 ...
25 ... """
26
27 >>> po_content = r"""
28 ... #: test.c:13
29 ... msgid "baz"
30 ... msgstr "%s"
31 ...
32 ... #, c-format
33 ... msgid "Foo %%s"
34 ... msgstr "%s"
35 ...
36 ... #, c-format
37 ... msgid "Singular %%d"
38 ... msgid_plural "Plural %%d"
39 ... msgstr[0] "%s"
40 ... msgstr[1] "%s"
41 ...
42 ... msgid "translator-credits"
43 ... msgstr "%s"
44 ... """
45
46 >>> potemplate_content = potemplate_header + po_content % (('',) * 5)
47 >>> pofile_eo_content = pofile_header + po_content % (
48 ... "baz eo", "Foo eo %s", "Singular eo %s", "Plural eo %s",
49 ... "helpful-eo@example.com")
50 >>> pofile_nl_content = pofile_header + po_content % (
51 ... "baz nl", "Foo nl %s", "Singular nl %s", "Plural nl %s",
52 ... "helpful-nl@example.com")
53
54There is annoying sample data in the queue that needs to be removed.
55
56 >>> from lp.translations.interfaces.translationimportqueue import (
57 ... ITranslationImportQueue, RosettaImportStatus)
58 >>> queue = getUtility(ITranslationImportQueue)
59 >>> for entry in queue:
60 ... queue.remove(entry)
61
62The files have been uploaded to the queue for a source package and have
63already been approved.
64
65 >>> from zope.security.proxy import removeSecurityProxy
66 >>> distroseries = factory.makeUbuntuDistroSeries()
67 >>> naked_distroseries = removeSecurityProxy(distroseries)
68 >>> naked_distroseries.distribution.official_rosetta = True
69 >>> sourcepackagename = factory.makeSourcePackageName()
70 >>> potemplate = factory.makePOTemplate(
71 ... distroseries=distroseries, sourcepackagename=sourcepackagename)
72 >>> pofile_eo = potemplate.newPOFile('eo')
73 >>> pofile_nl = potemplate.newPOFile('nl')
74
75 >>> from canonical.launchpad.interfaces.launchpad import (
76 ... ILaunchpadCelebrities)
77 >>> rosetta_experts = getUtility(ILaunchpadCelebrities).rosetta_experts
78
79 >>> template_entry = queue.addOrUpdateEntry(
80 ... potemplate.path, potemplate_content, True, potemplate.owner,
81 ... distroseries=distroseries, sourcepackagename=sourcepackagename,
82 ... potemplate=potemplate)
83 >>> pofile_eo_entry = queue.addOrUpdateEntry(
84 ... 'eo.po', pofile_eo_content, True, potemplate.owner,
85 ... distroseries=distroseries, sourcepackagename=sourcepackagename,
86 ... potemplate=potemplate, pofile=pofile_eo)
87 >>> pofile_nl_entry = queue.addOrUpdateEntry(
88 ... 'nl.po', pofile_nl_content, True, potemplate.owner,
89 ... distroseries=distroseries, sourcepackagename=sourcepackagename,
90 ... potemplate=potemplate, pofile=pofile_nl)
91 >>> transaction.commit()
92
93 >>> for entry in queue:
94 ... entry.setStatus(RosettaImportStatus.APPROVED, rosetta_experts)
95 >>> transaction.commit()
96
97As it happens, the administrator has blocked imports to the distroseries, e.g.
98because an in-database update of its translations has been scheduled
99and we don't want interference from queued imports while that happens.
100It doesn't really matter whether entries still get auto-approved, but
101we can't accept new translation imports just now.
102
103 >>> distroseries.defer_translation_imports
104 True
105
106 >>> from canonical.launchpad.scripts import FakeLogger
107 >>> from lp.translations.scripts.po_import import TranslationsImport
108 >>> import email
109 >>> from lp.services.mail import stub
110 >>> process = TranslationsImport('poimport', test_args=[])
111 >>> process.logger = FakeLogger()
112 >>> process.main()
113 DEBUG Starting the import process.
114 INFO No requests pending.
115
116When imports are allowed, the import script can do its work.
117
118 >>> naked_distroseries.defer_translation_imports = False
119
120 >>> process = TranslationsImport('poimport', test_args=[])
121 >>> process.logger = FakeLogger()
122 >>> process.main()
123 DEBUG Starting the import process.
124 INFO Importing: Template ...
125 INFO Importing: Esperanto (eo) ... of ...
126 INFO Importing: Dutch (nl) ... of ...
127 INFO Import requests completed.
128 DEBUG Finished the import process.
129
130The import script also generates an email similar to the ones we saw
131composed before, but also sends it.
132
133 >>> len(stub.test_emails)
134 1
135
136 >>> from_addr, to_addrs, raw_message = stub.test_emails.pop()
137 >>> msg = email.message_from_string(raw_message)
138 >>> print msg["Subject"]
139 Translation template import - ...
140
141 >>> print msg.get_payload(decode=True)
142 Hello ...,
143 <BLANKLINE>
144 On ..., you uploaded a translation
145 template for ... in Launchpad.
146 <BLANKLINE>
147 The template has now been imported successfully.
148 <BLANKLINE>
149 Thank you,
150 <BLANKLINE>
151 The Launchpad team
152
153The entries that remain in the queue as "imported" age over time.
154
155 >>> import datetime
156 >>> for entry in queue:
157 ... removeSecurityProxy(entry).date_status_changed -= (
158 ... datetime.timedelta(days=30))
159
160
161Now the queue gardener runs. This can happen anytime, since it's
162asynchronous to the po-import script. The script tries to approve any
163entries that have not been approved, but look like they could be,
164without human intervention. This involves a bit of guesswork about what
165the imported file is and where it belongs. It similarly blocks entries
166that it thinks should be blocked, and also purges deleted or completed
167entries from the queue. Running at this point, all it does is purge the
168two hand-approved Welsh translations that have just been imported.
169
170 >>> import logging
171 >>> from lp.testing.logger import MockLogger
172 >>> from lp.translations.scripts.import_queue_gardener import (
173 ... ImportQueueGardener)
174 >>> process = ImportQueueGardener('approver', test_args=[])
175 >>> process.logger = MockLogger()
176 >>> process.logger.setLevel(logging.INFO)
177 >>> process.main()
178 log> Removed 3 entries from the queue.
179 >>> transaction.commit()
180
181If users upload two versions of the same file, they are imported in the
182order in which they were uploaded.
183
184 >>> import pytz
185 >>> UTC = pytz.timezone('UTC')
186 >>> first_pofile_content = r'''
187 ... msgid ""
188 ... msgstr ""
189 ... "PO-Revision-Date: 2005-06-04 20:41+0100\n"
190 ... "Last-Translator: Foo <no-priv@canonical.com>\n"
191 ... "Content-Type: text/plain; charset=UTF-8\n"
192 ... "X-Rosetta-Export-Date: %s\n"
193 ...
194 ... msgid "Foo %%s"
195 ... msgstr "Bar"
196 ...
197 ... msgid "translator-credits"
198 ... msgstr "The world will never know."
199 ... ''' % datetime.datetime.now(UTC).isoformat()
200
201 >>> second_pofile_content = r'''
202 ... msgid ""
203 ... msgstr ""
204 ... "PO-Revision-Date: 2005-06-04 21:41+0100\n"
205 ... "Last-Translator: Jordi Mallach <jordi@canonical.com>\n"
206 ... "Content-Type: text/plain; charset=UTF-8\n"
207 ... "X-Rosetta-Export-Date: %s\n"
208 ...
209 ... msgid "Foo %%s"
210 ... msgstr "Bars"
211 ...
212 ... msgid "translator-credits"
213 ... msgstr "I'd like to thank John, Kathy, my pot plants, and all the..."
214 ... ''' % datetime.datetime.now(UTC).isoformat()
215
216Attach the first version of the file.
217
218 >>> entry = queue.addOrUpdateEntry(
219 ... pofile_eo.path, first_pofile_content, False, rosetta_experts,
220 ... sourcepackagename=sourcepackagename, distroseries=distroseries)
221 >>> transaction.commit()
222
223It's in the queue now.
224
225 >>> queue.countEntries()
226 1
227
228For the second version, we need a new importer.
229
230 >>> importer_person = factory.makePerson()
231
232Attach the second version of the file.
233
234 >>> entry = queue.addOrUpdateEntry(
235 ... pofile_eo.path, second_pofile_content, False, importer_person,
236 ... sourcepackagename=sourcepackagename, distroseries=distroseries)
237 >>> transaction.commit()
238
239It's in the queue now.
240
241 >>> queue.countEntries()
242 2
243 >>> print entry.status.name
244 NEEDS_REVIEW
245
246The queue gardener runs again. This time it sees the two submitted
247translations and approves them for import based on some heuristic
248intelligence.
249
250 >>> process = ImportQueueGardener('approver', test_args=[])
251 >>> process.logger = MockLogger()
252 >>> process.logger.setLevel(logging.INFO)
253 >>> process.main()
254 log> The automatic approval system approved some entries.
255 >>> print entry.status.name
256 APPROVED
257 >>> from canonical.launchpad.ftests import syncUpdate
258 >>> syncUpdate(entry)
259
260Now that these submissions have been approved, the next run of the
261import script picks them up and processes them.
262
263 >>> process = TranslationsImport('poimport', test_args=[])
264 >>> process.logger = FakeLogger()
265 >>> process.main()
266 DEBUG Starting the import process.
267 INFO Importing: Esperanto (eo) ... of ...
268 INFO Importing: Esperanto (eo) ... of ...
269 INFO Import requests completed.
270 DEBUG Finished the import process.
271
272 >>> print entry.status.name
273 IMPORTED
274 >>> syncUpdate(entry)
275
276And there are no more entries to import
277
278 >>> queue.getFirstEntryToImport() is None
279 True
280
281We've imported a new translation for "Foo %s."
282
283 >>> from lp.services.worlddata.interfaces.language import ILanguageSet
284 >>> esperanto = getUtility(ILanguageSet).getLanguageByCode('eo')
285 >>> foos = potemplate['Foo %s'].getLocalTranslationMessages(
286 ... potemplate, esperanto)
287 >>> sorted([foo.msgstr0.translation for foo in foos])
288 [u'Bar', u'Bars']
289
290Since this last upload was not the upstream one, however, its credits
291message translations were ignored.
292
293 >>> potmsgset = pofile_eo.potemplate.getPOTMsgSetByMsgIDText(
294 ... u'translator-credits')
295 >>> message = potmsgset.getCurrentTranslationMessage(
296 ... pofile_eo.potemplate, pofile_eo.language)
297 >>> message.msgstr0.translation
298 u'helpful-eo@example.com'
299 >>> list(potemplate['translator-credits'].getLocalTranslationMessages(
300 ... potemplate, esperanto))
301 []
302
303
304No Contact Address
305------------------
306
307Not every user has a valid email address. For instance, Kermit the
308Hermit has none at the moment.
309
310 >>> from canonical.launchpad.interfaces.emailaddress import (
311 ... EmailAddressStatus)
312 >>> from canonical.launchpad.helpers import get_contact_email_addresses
313 >>> hermit = factory.makePerson(
314 ... name='hermit', email_address_status=EmailAddressStatus.OLD)
315
316 >>> len(get_contact_email_addresses(hermit))
317 0
318
319Kermit uploads a translation, which gets approved.
320
321 >>> pofile = factory.makePOFile('lo', potemplate)
322 >>> entry = queue.addOrUpdateEntry(
323 ... 'lo.po', 'Invalid content', True, hermit,
324 ... pofile=pofile, potemplate=potemplate,
325 ... distroseries=distroseries, sourcepackagename=sourcepackagename)
326 >>> entry.setStatus(RosettaImportStatus.APPROVED, rosetta_experts)
327 >>> transaction.commit()
328
329The import fails. The importer would like to send Kermit an email about
330this, but is unable to. This is unfortunate, but does not faze the
331importer. It completes normally.
332
333 >>> process = TranslationsImport('poimport', test_args=[])
334 >>> process.logger = FakeLogger()
335 >>> process.main()
336 DEBUG Starting the import process.
337 INFO Importing: Lao ...
338 INFO Import requests completed.
339 DEBUG Finished the import process.
340
341 >>> print entry.status.name
342 FAILED
0343
=== modified file 'lib/lp/translations/doc/poimport.txt'
--- lib/lp/translations/doc/poimport.txt 2010-08-10 14:39:46 +0000
+++ lib/lp/translations/doc/poimport.txt 2010-09-21 16:23:42 +0000
@@ -1,23 +1,20 @@
1= PO Imports =1==========
2PO Imports
3==========
24
3The tale of a PO template and a PO file and how they get imported into5The tale of a PO template and a PO file and how they get imported into
4Rosetta.6Rosetta.
57
68Test Setup
7== Test Setup ==9==========
810
9Here are some imports we need to get this test running.11Here are some imports we need to get this test running.
1012
11 >>> from canonical.launchpad.ftests import syncUpdate13 >>> from canonical.launchpad.interfaces.launchpad import (
12 >>> from canonical.launchpad.interfaces import (14 ... ILaunchpadCelebrities)
13 ... ILanguageSet, ILaunchpadCelebrities, IPersonSet, IProductSet)15 >>> from lp.registry.interfaces.person import IPersonSet
14 >>> from lp.translations.interfaces.translationimportqueue import (16 >>> from lp.translations.interfaces.translationimportqueue import (
15 ... ITranslationImportQueue, RosettaImportStatus)17 ... ITranslationImportQueue, RosettaImportStatus)
16 >>> from lp.registry.model.sourcepackagename import SourcePackageName
17 >>> from lp.translations.model.potemplate import POTemplateSubset
18 >>> from lp.translations.scripts.po_import import TranslationsImport
19 >>> from lp.translations.scripts.import_queue_gardener import (
20 ... ImportQueueGardener)
21 >>> import datetime18 >>> import datetime
22 >>> import pytz19 >>> import pytz
23 >>> UTC = pytz.timezone('UTC')20 >>> UTC = pytz.timezone('UTC')
@@ -36,18 +33,15 @@
36 >>> login('carlos@canonical.com')33 >>> login('carlos@canonical.com')
3734
3835
39== Importing a Template ==36Importing a Template
37====================
4038
41Normal procedure is to import a template, followed by translations.39Normal procedure is to import a template, followed by translations.
42A template is created first. After that, imports are done using the40A template is created first. After that, imports are done using the
43POFile.importFromQueue and POTemplate.importFromQueue methods.41POFile.importFromQueue and POTemplate.importFromQueue methods.
4442
45 >>> from lp.registry.model.productrelease import ProductRelease43 >>> distroseries = factory.makeUbuntuDistroSeries()
46 >>> release = ProductRelease.get(3)44 >>> sourcepackagename = factory.makeSourcePackageName()
47 >>> release.productseries.product.name
48 u'firefox'
49 >>> series = release.productseries
50 >>> subset = POTemplateSubset(productseries=series)
5145
52Here's the person who'll be doing the import.46Here's the person who'll be doing the import.
5347
@@ -56,10 +50,8 @@
5650
57And this is the POTemplate where the import will be done.51And this is the POTemplate where the import will be done.
5852
59 >>> potemplate = subset.new(53 >>> potemplate = factory.makePOTemplate(
60 ... name='firefox',54 ... distroseries=distroseries, sourcepackagename=sourcepackagename,
61 ... translation_domain='firefox',
62 ... path='po/firefox.pot',
63 ... owner=person)55 ... owner=person)
64 >>> potemplate_id = potemplate.id56 >>> potemplate_id = potemplate.id
6557
@@ -110,7 +102,8 @@
110 >>> translation_import_queue = getUtility(ITranslationImportQueue)102 >>> translation_import_queue = getUtility(ITranslationImportQueue)
111 >>> entry = translation_import_queue.addOrUpdateEntry(103 >>> entry = translation_import_queue.addOrUpdateEntry(
112 ... potemplate.path, potemplate_contents, True, potemplate.owner,104 ... potemplate.path, potemplate_contents, True, potemplate.owner,
113 ... productseries=series, potemplate=potemplate)105 ... distroseries=distroseries, sourcepackagename=sourcepackagename,
106 ... potemplate=potemplate)
114107
115The file data is stored in the Librarian, so we have to commit the108The file data is stored in the Librarian, so we have to commit the
116transaction to make sure it's stored properly.109transaction to make sure it's stored properly.
@@ -143,13 +136,13 @@
143136
144A successful import is confirmed by email.137A successful import is confirmed by email.
145138
146 >>> subject139 >>> print subject
147 u'Translation template import - firefox in Mozilla Firefox trunk'140 Translation template import - ...
148 >>> print body141 >>> print body
149 Hello Mark Shuttleworth,142 Hello Mark Shuttleworth,
150 <BLANKLINE>143 <BLANKLINE>
151 On ..., you uploaded a translation144 On ..., you uploaded a translation
152 template for firefox in Mozilla Firefox trunk in Launchpad.145 template for ... in Launchpad.
153 <BLANKLINE>146 <BLANKLINE>
154 The template has now been imported successfully.147 The template has now been imported successfully.
155 <BLANKLINE>148 <BLANKLINE>
@@ -174,7 +167,8 @@
174 u'test.c:13'167 u'test.c:13'
175168
176169
177=== Import Preconditions ===170Import Preconditions
171====================
178172
179The API for POTemplate.importFromQueue demands a translation import173The API for POTemplate.importFromQueue demands a translation import
180queue entry to import.174queue entry to import.
@@ -195,12 +189,7 @@
195any other file would be an error.189any other file would be an error.
196190
197 >>> entry.setStatus(RosettaImportStatus.APPROVED, rosetta_experts)191 >>> entry.setStatus(RosettaImportStatus.APPROVED, rosetta_experts)
198 >>> from lp.translations.interfaces.potemplate import IPOTemplateSet192 >>> other_template = factory.makePOTemplate()
199 >>> other_product = getUtility(IProductSet).getByName('netapplet')
200 >>> other_productseries = other_product.getSeries('trunk')
201 >>> template_set = getUtility(IPOTemplateSet)
202 >>> other_template = template_set.getPOTemplateByPathAndOrigin(
203 ... 'po/netapplet.pot', productseries=other_productseries)
204 >>> other_template.importFromQueue(entry)193 >>> other_template.importFromQueue(entry)
205 Traceback (most recent call last):194 Traceback (most recent call last):
206 ...195 ...
@@ -208,7 +197,8 @@
208 to.197 to.
209198
210199
211== Importing a Translation ==200Importing a Translation
201=======================
212202
213Now let's get a PO file to import.203Now let's get a PO file to import.
214204
@@ -217,8 +207,8 @@
217207
218By default, we got a safe path to prevent collisions with other IPOFile.208By default, we got a safe path to prevent collisions with other IPOFile.
219209
220 >>> pofile.path210 >>> print pofile.path
221 u'po/firefox-cy.po'211 generic-string...-cy.po
222212
223Let's override the default good path with one we know is the right one.213Let's override the default good path with one we know is the right one.
224214
@@ -236,7 +226,8 @@
236 1226 1
237227
238228
239=== Import With Errors ===229Import With Errors
230------------------
240231
241Here are the contents of the file we'll be importing. It has some232Here are the contents of the file we'll be importing. It has some
242validation errors.233validation errors.
@@ -285,7 +276,8 @@
285276
286 >>> entry = translation_import_queue.addOrUpdateEntry(277 >>> entry = translation_import_queue.addOrUpdateEntry(
287 ... pofile.path, pofile_with_errors, True, person,278 ... pofile.path, pofile_with_errors, True, person,
288 ... productseries=series, potemplate=potemplate)279 ... distroseries=distroseries, sourcepackagename=sourcepackagename,
280 ... potemplate=potemplate)
289 >>> transaction.commit()281 >>> transaction.commit()
290282
291The guess IPOFile should be the same we already had.283The guess IPOFile should be the same we already had.
@@ -381,7 +373,7 @@
381 Hello Mark Shuttleworth,373 Hello Mark Shuttleworth,
382 <BLANKLINE>374 <BLANKLINE>
383 On ..., you uploaded 5375 On ..., you uploaded 5
384 Welsh (cy) translations for firefox in Mozilla Firefox trunk in Launchpad.376 Welsh (cy) translations for ... in Launchpad.
385 <BLANKLINE>377 <BLANKLINE>
386 There were problems with 1 of these translations.378 There were problems with 1 of these translations.
387 <BLANKLINE>379 <BLANKLINE>
@@ -410,7 +402,8 @@
410 msgstr "blah %i"402 msgstr "blah %i"
411403
412404
413=== Import With Warnings ===405Import With Warnings
406--------------------
414407
415The import may also succeed but produce syntax warnings. These need not408The import may also succeed but produce syntax warnings. These need not
416be tied to particular messages (they could be in the header, for409be tied to particular messages (they could be in the header, for
@@ -435,14 +428,14 @@
435 ... msgid "a"428 ... msgid "a"
436 ... msgstr "b"429 ... msgstr "b"
437 ... ''' % datetime.datetime.now(UTC).isoformat()430 ... ''' % datetime.datetime.now(UTC).isoformat()
438 >>> sumerian_pofile = potemplate.newPOFile('sux')431 >>> eo_pofile = potemplate.newPOFile('eo')
439 >>> warning_entry = translation_import_queue.addOrUpdateEntry(432 >>> warning_entry = translation_import_queue.addOrUpdateEntry(
440 ... 'sux.po', pofile_with_warning, False, potemplate.owner,433 ... 'eo.po', pofile_with_warning, False, potemplate.owner,
441 ... productseries=series, potemplate=potemplate,434 ... distroseries=distroseries, sourcepackagename=sourcepackagename,
442 ... pofile=sumerian_pofile)435 ... potemplate=potemplate, pofile=eo_pofile)
443 >>> transaction.commit()436 >>> transaction.commit()
444 >>> warning_entry.setStatus(RosettaImportStatus.APPROVED, rosetta_experts)437 >>> warning_entry.setStatus(RosettaImportStatus.APPROVED, rosetta_experts)
445 >>> (subject, message) = sumerian_pofile.importFromQueue(warning_entry)438 >>> (subject, message) = eo_pofile.importFromQueue(warning_entry)
446439
447The warning is noted in the confirmation email. Note that this440The warning is noted in the confirmation email. Note that this
448particular warning condition is recognized fairly late, so the line441particular warning condition is recognized fairly late, so the line
@@ -473,7 +466,8 @@
473 >>> warning_entry.setStatus(RosettaImportStatus.DELETED, rosetta_experts)466 >>> warning_entry.setStatus(RosettaImportStatus.DELETED, rosetta_experts)
474467
475468
476=== Import Without Errors ===469Import Without Errors
470---------------------
477471
478Now, let's import one without errors.472Now, let's import one without errors.
479473
@@ -495,7 +489,8 @@
495 ... ''' % datetime.datetime.now(UTC).isoformat()489 ... ''' % datetime.datetime.now(UTC).isoformat()
496 >>> entry = translation_import_queue.addOrUpdateEntry(490 >>> entry = translation_import_queue.addOrUpdateEntry(
497 ... pofile.path, pofile_without_errors, True, rosetta_experts,491 ... pofile.path, pofile_without_errors, True, rosetta_experts,
498 ... productseries=series, potemplate=potemplate)492 ... distroseries=distroseries, sourcepackagename=sourcepackagename,
493 ... potemplate=potemplate)
499 >>> transaction.commit()494 >>> transaction.commit()
500495
501The new upload clears the entry's error_output.496The new upload clears the entry's error_output.
@@ -580,7 +575,8 @@
580 u'helpful@example.com'575 u'helpful@example.com'
581576
582577
583=== Import Preconditions ===578Import Preconditions
579====================
584580
585The API for POFile.importFromQueue demands a translation import queue581The API for POFile.importFromQueue demands a translation import queue
586entry to import.582entry to import.
@@ -617,296 +613,28 @@
617 to.613 to.
618614
619615
620== Cron Scripts ==616Plural forms handling
621617=====================
622We tested already that the functionality works. Now it's time to know
623if the cronscript has any problem.
624
625First, we are going to reactivate the entries that were already
626imported or failed. Note that we'll only reactivate the entries we use
627in this test; We don't touch entries that were in the queue previously.
628
629 >>> for entry in translation_import_queue:
630 ... if (entry.status == RosettaImportStatus.IMPORTED or
631 ... entry.status == RosettaImportStatus.FAILED) and (
632 ... entry.productseries == series):
633 ... entry.setStatus(RosettaImportStatus.APPROVED, rosetta_experts)
634 ... syncUpdate(entry)
635 >>> transaction.commit()
636
637And run the import script.
638
639 >>> import email
640 >>> from lp.services.mail import stub
641 >>> process = TranslationsImport('poimport', test_args=[])
642 >>> process.logger = FakeLogger()
643 >>> process.main()
644 DEBUG Starting the import process.
645 INFO Importing: Template "firefox" in Mozilla Firefox trunk
646 INFO Importing: Welsh (cy) ... of firefox in Mozilla Firefox trunk
647 INFO Importing: Welsh (cy) ... of firefox in Mozilla Firefox trunk
648 INFO Import requests completed.
649 DEBUG Finished the import process.
650
651The import script also generates an email similar to the ones we saw
652composed before, but also sends it.
653
654 >>> len(stub.test_emails)
655 1
656
657 >>> from_addr, to_addrs, raw_message = stub.test_emails.pop()
658 >>> msg = email.message_from_string(raw_message)
659 >>> msg["Subject"]
660 'Translation template import - firefox in Mozilla Firefox trunk'
661
662 >>> print msg.get_payload(decode=True)
663 Hello Mark Shuttleworth,
664 <BLANKLINE>
665 On ..., you uploaded a translation
666 template for firefox in Mozilla Firefox trunk in Launchpad.
667 <BLANKLINE>
668 The template has now been imported successfully.
669 <BLANKLINE>
670 Thank you,
671 <BLANKLINE>
672 The Launchpad team
673
674Now the queue gardener runs. This can happen anytime, since it's
675asynchronous to the po-import script. The script tries to approve any
676entries that have not been approved, but look like they could be,
677without human intervention. This involves a bit of guesswork about what
678the imported file is and where it belongs. It similarly blocks entries
679that it thinks should be blocked, and also purges deleted or completed
680entries from the queue. Running at this point, all it does is purge the
681two hand-approved Welsh translations that have just been imported.
682
683 >>> import logging
684 >>> from lp.testing.logger import MockLogger
685 >>> process = ImportQueueGardener('approver', test_args=[])
686 >>> process.logger = MockLogger()
687 >>> process.logger.setLevel(logging.INFO)
688 >>> process.main()
689 log> Removed 2 entries from the queue.
690 >>> transaction.commit()
691
692If users upload two versions of the same file, they are imported in the
693order in which they were uploaded.
694
695 >>> first_pofile_content = r'''
696 ... msgid ""
697 ... msgstr ""
698 ... "PO-Revision-Date: 2005-06-04 20:41+0100\n"
699 ... "Last-Translator: Foo <no-priv@canonical.com>\n"
700 ... "Content-Type: text/plain; charset=UTF-8\n"
701 ... "X-Rosetta-Export-Date: %s\n"
702 ...
703 ... msgid "Foo %%s"
704 ... msgstr "Bar"
705 ...
706 ... msgid "translator-credits"
707 ... msgstr "The world will never know."
708 ... ''' % datetime.datetime.now(UTC).isoformat()
709
710 >>> second_pofile_content = r'''
711 ... msgid ""
712 ... msgstr ""
713 ... "PO-Revision-Date: 2005-06-04 21:41+0100\n"
714 ... "Last-Translator: Jordi Mallach <jordi@canonical.com>\n"
715 ... "Content-Type: text/plain; charset=UTF-8\n"
716 ... "X-Rosetta-Export-Date: %s\n"
717 ...
718 ... msgid "Foo %%s"
719 ... msgstr "Bars"
720 ...
721 ... msgid "translator-credits"
722 ... msgstr "I'd like to thank John, Kathy, my pot plants, and all the..."
723 ... ''' % datetime.datetime.now(UTC).isoformat()
724
725We flush the entry contents.
726
727 >>> for entry in translation_import_queue:
728 ... translation_import_queue.remove(entry)
729 >>> translation_import_queue.countEntries()
730 0
731
732Attach the first version of the file.
733
734 >>> entry = translation_import_queue.addOrUpdateEntry(
735 ... pofile.path, first_pofile_content, False, rosetta_experts,
736 ... sourcepackagename=pofile.potemplate.sourcepackagename,
737 ... distroseries=pofile.potemplate.distroseries,
738 ... productseries=pofile.potemplate.productseries)
739 >>> transaction.commit()
740
741It's in the queue now.
742
743 >>> translation_import_queue.countEntries()
744 1
745
746For the second version, we need a new importer, in this case, Jordi.
747
748 >>> jordi = person_set.getByName('jordi')
749
750Attach the second version of the file.
751
752 >>> entry = translation_import_queue.addOrUpdateEntry(
753 ... pofile.path, second_pofile_content, False, jordi,
754 ... sourcepackagename=pofile.potemplate.sourcepackagename,
755 ... distroseries=pofile.potemplate.distroseries,
756 ... productseries=pofile.potemplate.productseries)
757 >>> transaction.commit()
758
759It's in the queue now.
760
761 >>> translation_import_queue.countEntries()
762 2
763 >>> print entry.status.name
764 NEEDS_REVIEW
765
766The queue gardener runs again. This time it sees the two submitted
767translations and approves them for import based on some heuristic
768intelligence.
769
770 >>> process = ImportQueueGardener('approver', test_args=[])
771 >>> process.logger = MockLogger()
772 >>> process.logger.setLevel(logging.INFO)
773 >>> process.main()
774 log> The automatic approval system approved some entries.
775 >>> print entry.status.name
776 APPROVED
777 >>> syncUpdate(entry)
778
779Now that these submissions have been approved, the next run of the
780import script picks them up and processes them.
781
782 >>> process = TranslationsImport('poimport', test_args=[])
783 >>> process.logger = FakeLogger()
784 >>> process.main()
785 DEBUG Starting the import process.
786 INFO Importing: Welsh (cy) ... of firefox in Mozilla Firefox trunk
787 INFO Importing: Welsh (cy) ... of firefox in Mozilla Firefox trunk
788 INFO Import requests completed.
789 DEBUG Finished the import process.
790
791 >>> print entry.status.name
792 IMPORTED
793 >>> syncUpdate(entry)
794
795And there are no more entries to import
796
797 >>> translation_import_queue.getFirstEntryToImport() is None
798 True
799
800We've imported a new translation for "Foo %s."
801
802 >>> welsh = getUtility(ILanguageSet).getLanguageByCode('cy')
803 >>> foos = potemplate['Foo %s'].getLocalTranslationMessages(
804 ... potemplate, welsh)
805 >>> sorted([foo.msgstr0.translation for foo in foos])
806 [u'Bar', u'Bars', u'blah %i']
807
808Since this last upload was not the upstream one, however, its credits
809message translations were ignored.
810
811 >>> message = get_pofile_translation_message(
812 ... pofile, u'translator-credits')
813 >>> message.msgstr0.translation
814 u'helpful@example.com'
815 >>> list(potemplate['translator-credits'].getLocalTranslationMessages(
816 ... potemplate, welsh))
817 []
818
819Imports so far have been associated with a product series. We can also
820submit translations for a distroseries.
821
822 >>> from lp.registry.interfaces.distribution import (
823 ... IDistributionSet)
824 >>> ubuntu = getUtility(IDistributionSet).getByName('ubuntu')
825 >>> warty = ubuntu.getSeries('warty')
826 >>> print warty.name
827 warty
828 >>> firefox_name = SourcePackageName.byName('mozilla-firefox')
829 >>> subset = POTemplateSubset(sourcepackagename=firefox_name,
830 ... distroseries=warty)
831 >>> potemplate = subset.new(
832 ... name='firefox-warty',
833 ... translation_domain='firefox-warty',
834 ... path='po/firefox.pot',
835 ... owner=person)
836
837As it happens, the administrator has blocked imports to warty, e.g.
838because an in-database update of its translations has been scheduled
839and we don't want interference from queued imports while that happens.
840It doesn't really matter whether entries still get auto-approved, but
841we can't accept new translation imports just now.
842
843 >>> warty.defer_translation_imports = True
844 >>> syncUpdate(warty)
845
846Nevertheless, someone submits an import request for warty, not knowing
847or caring that imports are deferred. The entry still gets approved as
848normal:
849
850 >>> entry = translation_import_queue.addOrUpdateEntry(
851 ... potemplate.path, potemplate_contents, True, potemplate.owner,
852 ... sourcepackagename=firefox_name, distroseries=warty,
853 ... potemplate=potemplate)
854 >>> entry.setStatus(RosettaImportStatus.APPROVED, rosetta_experts)
855 >>> syncUpdate(entry)
856 >>> transaction.commit()
857
858Since imports for warty are suspended, and the only entry we happen to
859have waiting right now is for warty, the queue has no importable
860entries for us.
861
862 >>> warty.getFirstEntryToImport() is None
863 True
864
865So if we try to import now, nothing happens. Our request remains on the
866queue, but doesn't become a candidate for processing until warty
867imports are resumed.
868
869 >>> process = TranslationsImport('poimport', test_args=[])
870 >>> process.logger = FakeLogger()
871 >>> process.main()
872 DEBUG Starting the import process.
873 INFO No requests pending.
874
875 >>> print entry.status.name
876 APPROVED
877
878Once imports are allowed again, the import is done after all.
879
880 >>> warty.defer_translation_imports = False
881 >>> syncUpdate(warty)
882 >>> (subject, body) = potemplate.importFromQueue(entry, FakeLogger())
883
884 >>> print entry.status.name
885 IMPORTED
886
887
888== Plural forms handling ==
889618
890Apart from the basic plural form handling, which is documented above as619Apart from the basic plural form handling, which is documented above as
891part of the import process, there are some peculiarities with importing620part of the import process, there are some peculiarities with importing
892plural forms we want documented as well.621plural forms we want documented as well.
893622
894For a language such as Divehi, which has no plural forms defined, we623For a language that has no plural forms defined, we
895default to two plural forms (the most common value for the number of624default to two plural forms (the most common value for the number of
896plural forms).625plural forms).
897626
898 >>> divehi = getUtility(ILanguageSet)['dv']627 >>> language = factory.makeLanguage()
899 >>> print divehi.pluralforms628 >>> print language.pluralforms
900 None629 None
901630
902 >>> firefox = getUtility(IProductSet).getByName('firefox')631 >>> potemplate = factory.makePOTemplate(
903 >>> firefox_trunk = firefox.getSeries('trunk')632 ... distroseries=distroseries, sourcepackagename=sourcepackagename)
904 >>> firefox_potemplate = firefox_trunk.getPOTemplate('firefox')633 >>> pofile = potemplate.newPOFile(language.code)
905 >>> firefox_dv = firefox_potemplate.newPOFile(divehi.code)634 >>> pofile.plural_forms
906 >>> firefox_dv.plural_forms
907 2635 2
908636
909We'll import a POFile with 3 plural forms into Divehi POFile:637We'll import a POFile with 3 plural forms into this POFile:
910638
911 >>> pofile_with_plurals = r'''639 >>> pofile_with_plurals = r'''
912 ... msgid ""640 ... msgid ""
@@ -925,16 +653,18 @@
925 ... msgstr[2] "Third form %%d"653 ... msgstr[2] "Third form %%d"
926 ... ''' % datetime.datetime.now(UTC).isoformat()654 ... ''' % datetime.datetime.now(UTC).isoformat()
927655
928We now import this POFile as Divehi translation of Firefox trunk:656We now import this POFile as this language's translation for the soure
657package:
929658
930 >>> entry = translation_import_queue.addOrUpdateEntry(659 >>> entry = translation_import_queue.addOrUpdateEntry(
931 ... firefox_dv.path, pofile_with_plurals, True, person,660 ... pofile.path, pofile_with_plurals, True, person,
932 ... productseries=firefox_trunk, potemplate=firefox_potemplate)661 ... distroseries=distroseries, sourcepackagename=sourcepackagename,
662 ... potemplate=potemplate)
933 >>> # Allow Librarian to see the change.663 >>> # Allow Librarian to see the change.
934 >>> transaction.commit()664 >>> transaction.commit()
935 >>> entry.pofile = firefox_dv665 >>> entry.pofile = pofile
936 >>> entry.setStatus(RosettaImportStatus.APPROVED, rosetta_experts)666 >>> entry.setStatus(RosettaImportStatus.APPROVED, rosetta_experts)
937 >>> (subject, body) = firefox_dv.importFromQueue(entry, FakeLogger())667 >>> (subject, body) = pofile.importFromQueue(entry, FakeLogger())
938 >>> flush_database_updates()668 >>> flush_database_updates()
939 >>> print entry.status.name669 >>> print entry.status.name
940 IMPORTED670 IMPORTED
@@ -943,21 +673,22 @@
943translations (which is a default when the language has no plural forms673translations (which is a default when the language has no plural forms
944specified):674specified):
945675
946 >>> potmsgset_plural = firefox_potemplate.getPOTMsgSetByMsgIDText(676 >>> potmsgset_plural = potemplate.getPOTMsgSetByMsgIDText(
947 ... u'Singular %d', u'Plural %d')677 ... u'Singular %d', u'Plural %d')
948 >>> current_dv = potmsgset_plural.getCurrentTranslationMessage(678 >>> current = potmsgset_plural.getCurrentTranslationMessage(
949 ... firefox_potemplate, divehi)679 ... potemplate, language)
950 >>> current_dv.translations680 >>> current.translations
951 [u'First form %d', u'Second form %d']681 [u'First form %d', u'Second form %d']
952682
953However, even the third form will be imported into database (this is683However, even the third form will be imported into database (this is
954useful for when we finally define the number of plural forms for the684useful for when we finally define the number of plural forms for the
955language, we should not have to reimport all translations):685language, we should not have to reimport all translations):
956686
957 >>> current_dv.msgstr2.translation687 >>> current.msgstr2.translation
958 u'Third form %d'688 u'Third form %d'
959689
960== Upstream import notifications ==690Upstream import notifications
691=============================
961692
962Add an upstream POFile import (i.e. from a package or bzr branch),693Add an upstream POFile import (i.e. from a package or bzr branch),
963approve and import it.694approve and import it.
@@ -974,12 +705,12 @@
974 ... msgid "foo"705 ... msgid "foo"
975 ... msgstr "blah"706 ... msgstr "blah"
976 ... '''707 ... '''
977 >>> pofile = factory.makePOFile('sr')708 >>> pofile = factory.makePOFile('sr', potemplate=potemplate)
978 >>> from_upstream = True709 >>> from_upstream = True
979 >>> entry = translation_import_queue.addOrUpdateEntry(710 >>> entry = translation_import_queue.addOrUpdateEntry(
980 ... pofile.path, pofile_contents, from_upstream, person,711 ... pofile.path, pofile_contents, from_upstream, person,
981 ... productseries=pofile.potemplate.productseries,712 ... distroseries=distroseries, sourcepackagename=sourcepackagename,
982 ... potemplate=pofile.potemplate, pofile=pofile)713 ... potemplate=potemplate, pofile=pofile)
983 >>> transaction.commit()714 >>> transaction.commit()
984 >>> entry.setStatus(RosettaImportStatus.APPROVED, rosetta_experts)715 >>> entry.setStatus(RosettaImportStatus.APPROVED, rosetta_experts)
985 >>> (subject, message) = pofile.importFromQueue(entry)716 >>> (subject, message) = pofile.importFromQueue(entry)
@@ -997,8 +728,8 @@
997 >>> pofile_contents = pofile_contents[:-2]728 >>> pofile_contents = pofile_contents[:-2]
998 >>> entry = translation_import_queue.addOrUpdateEntry(729 >>> entry = translation_import_queue.addOrUpdateEntry(
999 ... pofile.path, pofile_contents, from_upstream, person,730 ... pofile.path, pofile_contents, from_upstream, person,
1000 ... productseries=pofile.potemplate.productseries,731 ... distroseries=distroseries, sourcepackagename=sourcepackagename,
1001 ... potemplate=pofile.potemplate, pofile=pofile)732 ... potemplate=potemplate, pofile=pofile)
1002 >>> transaction.commit()733 >>> transaction.commit()
1003 >>> entry.setStatus(RosettaImportStatus.APPROVED, rosetta_experts)734 >>> entry.setStatus(RosettaImportStatus.APPROVED, rosetta_experts)
1004 >>> (subject, message) = pofile.importFromQueue(entry)735 >>> (subject, message) = pofile.importFromQueue(entry)
@@ -1010,44 +741,3 @@
1010 >>> subject741 >>> subject
1011 u'Import problem - Serbian (sr) - ...'742 u'Import problem - Serbian (sr) - ...'
1012743
1013
1014No Contact Address
1015------------------
1016
1017Not every user has a valid email address. For instance, Kermit the
1018Hermit has none at the moment.
1019
1020 >>> from canonical.launchpad.interfaces.emailaddress import (
1021 ... EmailAddressStatus)
1022 >>> from canonical.launchpad.helpers import get_contact_email_addresses
1023 >>> hermit = factory.makePerson(
1024 ... name='hermit', email_address_status=EmailAddressStatus.OLD)
1025
1026 >>> len(get_contact_email_addresses(hermit))
1027 0
1028
1029Kermit uploads a translation, which gets approved.
1030
1031 >>> pofile = factory.makePOFile('lo')
1032
1033 >>> entry = translation_import_queue.addOrUpdateEntry(
1034 ... 'lo.po', 'Invalid content', True, hermit,
1035 ... pofile=pofile, potemplate=pofile.potemplate,
1036 ... productseries=pofile.potemplate.productseries)
1037 >>> entry.setStatus(RosettaImportStatus.APPROVED, rosetta_experts)
1038 >>> transaction.commit()
1039
1040The import fails. The importer would like to send Kermit an email about
1041this, but is unable to. This is unfortunate, but does not faze the
1042importer. It completes normally.
1043
1044 >>> process = TranslationsImport('poimport', test_args=[])
1045 >>> process.logger = FakeLogger()
1046 >>> process.main()
1047 DEBUG Starting the import process.
1048 INFO Importing: Lao ...
1049 INFO Import requests completed.
1050 DEBUG Finished the import process.
1051
1052 >>> print entry.status.name
1053 FAILED

Subscribers

People subscribed via source and target branches