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