Merge lp:~jtv/launchpad/bug-286237 into lp:launchpad
- bug-286237
- Merge into devel
Status: | Merged | ||||||||
---|---|---|---|---|---|---|---|---|---|
Approved by: | Jeroen T. Vermeulen | ||||||||
Approved revision: | no longer in the source branch. | ||||||||
Merged at revision: | not available | ||||||||
Proposed branch: | lp:~jtv/launchpad/bug-286237 | ||||||||
Merge into: | lp:launchpad | ||||||||
Diff against target: |
754 lines (+279/-94) 14 files modified
lib/lp/translations/browser/tests/test_baseexportview.py (+3/-2) lib/lp/translations/doc/poexport-queue.txt (+6/-5) lib/lp/translations/doc/poexport-request-productseries.txt (+3/-2) lib/lp/translations/doc/poexport-request.txt (+8/-3) lib/lp/translations/doc/poexportqueue-replication-lag.txt (+89/-0) lib/lp/translations/doc/potmsgset.txt (+12/-8) lib/lp/translations/interfaces/poexportrequest.py (+23/-7) lib/lp/translations/interfaces/potmsgset.py (+14/-8) lib/lp/translations/model/poexportrequest.py (+68/-31) lib/lp/translations/model/potmsgset.py (+8/-1) lib/lp/translations/scripts/po_export_queue.py (+10/-24) lib/lp/translations/tests/test_potmsgset.py (+23/-1) lib/lp/translations/tests/test_suggestions.py (+8/-1) lib/lp/translations/tests/test_translatablemessage.py (+4/-1) |
||||||||
To merge this branch: | bzr merge lp:~jtv/launchpad/bug-286237 | ||||||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Guilherme Salgado (community) | Approve | ||
Canonical Launchpad Engineering | code | Pending | |
Review via email: mp+20767@code.launchpad.net |
Commit message
Use slave store for suggestions search & translations export.
Description of the change
= Bugs 286237, =
We're still getting some timeouts from the +translate page because fetching external suggestions is costly, and we're still occasionally seeing big, slow exports. Load on the master database peaks higher than Stuart would like, whereas the slaves don't have this problem and can scale out if needed.
This branch moves these tasks to the slave store. This is very easy for external suggestions, and not too hard but still a bit of work for the export queue. (Luckily I'm spending a lot of time waiting for make schema etc. today).
Some background about the export queue: it lives in the POExportRequest table. The app server adds requests to this queue. If often adds many at a time to cover multiple files; simultaneous requests from the same user are bundled. The export script takes a bundle of requests off the queue and produces exports for them based on the translations data we have.
(A few years ago we modified the export script to service just one request at a time. That was a desperation measure against load problems we had at the time, mainly because of OpenOffice.org. In the meantime, exporting full translations for an Ubuntu package has been restricted a bit; database servers have been upgraded; many problems have been addressed; and OpenOffice.org is no longer translated in Ubuntu. So I made the script do all exports at once again. Not waiting 10 minutes or whatever between exports will probably do wonders for perceived speed of the exports.)
In order to prevent double exports caused by replication lag, the export script now first checks what the oldest live request on the master is. It then considers only that request and newer ones on the slave; older ones have presumably already been deleted, but the slave hasn't noticed it yet. When it's done, it deletes the requests it's done with from the master, and so they certainly won't be selected again on the next call. An extra little doctest goes through this.
I split the queue method popRequest into getRequest and removeRequest. This means that the script only tries to delete the request records after it's processed them. This doesn't matter functionally (everything only happens at commit anyway), but shortens the timespan during which the script holds a transaction with pending changes on the master. If the script holds uncommitted deletes on the queue while processing a request, it may block out app servers trying to add to the queue. Doing it this way should make things run a little more smoothly.
The SQL has also been Stormicated. Deleting items wasn't such smooth sailing: Storm informed me that deleting based on is_in or other expressions is not supported yet. Therefore I executed some raw SQL on the master store (but again in the Storm way, not using cursor). The tests need a few more real commits to allow the master and slave stores to synchronize.
I haven't bothered to recover after an error. If something goes wrong, backing off a bit and waiting for the next cron run is probably a sensible thing to do. If it really piles up, the export pages will mention it to users.
No lint. To test:
{{{
./bin/test -vv -t 'translations.
}}}
To Q/A, verify that external suggestions still appear on +translate pages. Also, request an export on staging and ask for the export script to be run:
{{{
cronscripts/
}}}
This will print a URL to a Librarian file with the exports. Verify that these are alright, e.g. by comparing to a similar export from production.
Jeroen
Jeroen T. Vermeulen (jtv) wrote : | # |
Thanks for the review! You've brought on some good improvements there:
> > === modified file 'lib/lp/
> > --- lib/lp/
> 17:06:01 +0000
> > +++ lib/lp/
> 17:38:34 +0000
> > @@ -15,13 +16,14 @@
> > ProductSeriesTr
> > from lp.translations
> > TranslationFile
> > +from lp.translations
> > from lp.testing import TestCaseWithFactory
> >
> >
> > def wipe_queue(queue):
> > """Erase all export queue entries."""
> > - while queue.entry_count > 0:
> > - queue.popRequest()
> > + queue_ids = IMasterStore(
>
> You don't need to store the queue ids here, do you?
You're right, I don't, and this can be cleaned up. Just did that.
> > === modified file 'lib/lp/
> > --- lib/lp/
> +0000
> > +++ lib/lp/
> +0000
> > @@ -53,9 +53,10 @@
> >
> > Now we request that the queue be processed.
> >
> > - >>> from lp.testing.
> > + >>> import transaction
> > >>> from lp.translations
> > - >>> process_
> > + >>> transaction.
> > + >>> process_
> > log> Exporting objects for Happy Downloader, related to template pmount
> > in Ubuntu Hoary package "pmount"
> > log> Stored file at http://
> > @@ -185,7 +186,8 @@
> > >>> from lp.translations
> > ... TranslationFile
> > >>> request_
> TranslationFile
> > - >>> process_
> > + >>> transaction.
>
> Do you think it'd be worth adding comments explaining why we need all
> these commits?
Definitely. I added a brief note to the first one.
> > === added file 'lib/lp/
> > --- lib/lp/
> 00:00:00 +0000
> > +++ lib/lp/
> 17:38:34 +0000
> > @@ -0,0 +1,89 @@
> > += Replication Lag and the Export Queue =
> > +
> > +Due to replication lag it's possible for the export queue to see a
> > +request on the slave store that it actually just removed from the master
> > +store.
> > +
> > +We start our story with an empty export queue.
> > +
> > + >>> from datetime import timedelta
> > + >>> import transaction
> > + >>> from zope.component import getUtility
> > + >>> from canonical.
> > + >>> from lp.translations
> > + ... IPOExportReques
> > + >>> from lp.translations
Preview Diff
1 | === modified file 'lib/lp/translations/browser/tests/test_baseexportview.py' | |||
2 | --- lib/lp/translations/browser/tests/test_baseexportview.py 2010-02-19 17:06:01 +0000 | |||
3 | +++ lib/lp/translations/browser/tests/test_baseexportview.py 2010-03-06 06:29:39 +0000 | |||
4 | @@ -7,6 +7,7 @@ | |||
5 | 7 | import transaction | 7 | import transaction |
6 | 8 | import unittest | 8 | import unittest |
7 | 9 | 9 | ||
8 | 10 | from canonical.launchpad.interfaces.lpstorm import IMasterStore | ||
9 | 10 | from canonical.launchpad.webapp.servers import LaunchpadTestRequest | 11 | from canonical.launchpad.webapp.servers import LaunchpadTestRequest |
10 | 11 | from canonical.testing import ZopelessDatabaseLayer | 12 | from canonical.testing import ZopelessDatabaseLayer |
11 | 12 | from lp.translations.browser.sourcepackage import ( | 13 | from lp.translations.browser.sourcepackage import ( |
12 | @@ -15,13 +16,13 @@ | |||
13 | 15 | ProductSeriesTranslationsExportView) | 16 | ProductSeriesTranslationsExportView) |
14 | 16 | from lp.translations.interfaces.translationfileformat import ( | 17 | from lp.translations.interfaces.translationfileformat import ( |
15 | 17 | TranslationFileFormat) | 18 | TranslationFileFormat) |
16 | 19 | from lp.translations.model.poexportrequest import POExportRequest | ||
17 | 18 | from lp.testing import TestCaseWithFactory | 20 | from lp.testing import TestCaseWithFactory |
18 | 19 | 21 | ||
19 | 20 | 22 | ||
20 | 21 | def wipe_queue(queue): | 23 | def wipe_queue(queue): |
21 | 22 | """Erase all export queue entries.""" | 24 | """Erase all export queue entries.""" |
24 | 23 | while queue.entry_count > 0: | 25 | IMasterStore(POExportRequest).execute("DELETE FROM POExportRequest") |
23 | 24 | queue.popRequest() | ||
25 | 25 | 26 | ||
26 | 26 | 27 | ||
27 | 27 | class BaseExportViewMixin(TestCaseWithFactory): | 28 | class BaseExportViewMixin(TestCaseWithFactory): |
28 | 28 | 29 | ||
29 | === modified file 'lib/lp/translations/doc/poexport-queue.txt' | |||
30 | --- lib/lp/translations/doc/poexport-queue.txt 2010-02-19 16:02:16 +0000 | |||
31 | +++ lib/lp/translations/doc/poexport-queue.txt 2010-03-06 06:29:39 +0000 | |||
32 | @@ -275,7 +275,8 @@ | |||
33 | 275 | 275 | ||
34 | 276 | Once the queue is processed, the queue is empty again. | 276 | Once the queue is processed, the queue is empty again. |
35 | 277 | 277 | ||
37 | 278 | >>> process_queue(fake_transaction, logging.getLogger()) | 278 | >>> transaction.commit() |
38 | 279 | >>> process_queue(transaction, logging.getLogger()) | ||
39 | 279 | INFO:...Stored file at http://.../po_evolution-2.2.pot | 280 | INFO:...Stored file at http://.../po_evolution-2.2.pot |
40 | 280 | 281 | ||
41 | 281 | >>> export_request_set.entry_count | 282 | >>> export_request_set.entry_count |
42 | @@ -333,10 +334,10 @@ | |||
43 | 333 | 334 | ||
44 | 334 | >>> export_request_set.addRequest( | 335 | >>> export_request_set.addRequest( |
45 | 335 | ... carlos, pofiles=[pofile], format=TranslationFileFormat.PO) | 336 | ... carlos, pofiles=[pofile], format=TranslationFileFormat.PO) |
47 | 336 | >>> process_queue(fake_transaction, logging.getLogger()) | 337 | >>> transaction.commit() |
48 | 338 | >>> process_queue(transaction, logging.getLogger()) | ||
49 | 337 | INFO:root:Stored file at http://...eo.po | 339 | INFO:root:Stored file at http://...eo.po |
50 | 338 | 340 | ||
51 | 339 | >>> transaction.commit() | ||
52 | 340 | >>> print get_newest_librarian_file().read() | 341 | >>> print get_newest_librarian_file().read() |
53 | 341 | # Esperanto translation for ... | 342 | # Esperanto translation for ... |
54 | 342 | ... | 343 | ... |
55 | @@ -354,10 +355,10 @@ | |||
56 | 354 | 355 | ||
57 | 355 | >>> export_request_set.addRequest( | 356 | >>> export_request_set.addRequest( |
58 | 356 | ... carlos, pofiles=[pofile], format=TranslationFileFormat.POCHANGED) | 357 | ... carlos, pofiles=[pofile], format=TranslationFileFormat.POCHANGED) |
60 | 357 | >>> process_queue(fake_transaction, logging.getLogger()) | 358 | >>> transaction.commit() |
61 | 359 | >>> process_queue(transaction, logging.getLogger()) | ||
62 | 358 | INFO:root:Stored file at http://...eo.po | 360 | INFO:root:Stored file at http://...eo.po |
63 | 359 | 361 | ||
64 | 360 | >>> transaction.commit() | ||
65 | 361 | >>> print get_newest_librarian_file().read() | 362 | >>> print get_newest_librarian_file().read() |
66 | 362 | # IMPORTANT: This file does NOT contain a complete PO file structure. | 363 | # IMPORTANT: This file does NOT contain a complete PO file structure. |
67 | 363 | # DO NOT attempt to import this file back into Launchpad. | 364 | # DO NOT attempt to import this file back into Launchpad. |
68 | 364 | 365 | ||
69 | === modified file 'lib/lp/translations/doc/poexport-request-productseries.txt' | |||
70 | --- lib/lp/translations/doc/poexport-request-productseries.txt 2010-02-19 16:54:42 +0000 | |||
71 | +++ lib/lp/translations/doc/poexport-request-productseries.txt 2010-03-06 06:29:39 +0000 | |||
72 | @@ -35,11 +35,12 @@ | |||
73 | 35 | Now we request that the queue be processed. | 35 | Now we request that the queue be processed. |
74 | 36 | 36 | ||
75 | 37 | >>> import logging | 37 | >>> import logging |
77 | 38 | >>> from lp.testing.faketransaction import FakeTransaction | 38 | >>> import transaction |
78 | 39 | >>> from lp.translations.scripts.po_export_queue import process_queue | 39 | >>> from lp.translations.scripts.po_export_queue import process_queue |
79 | 40 | >>> logger = MockLogger() | 40 | >>> logger = MockLogger() |
80 | 41 | >>> logger.setLevel(logging.DEBUG) | 41 | >>> logger.setLevel(logging.DEBUG) |
82 | 42 | >>> process_queue(FakeTransaction(), logger) | 42 | >>> transaction.commit() |
83 | 43 | >>> process_queue(transaction, logger) | ||
84 | 43 | log> Exporting objects for ..., related to template evolution-2.2 in | 44 | log> Exporting objects for ..., related to template evolution-2.2 in |
85 | 44 | Evolution trunk | 45 | Evolution trunk |
86 | 45 | log> Exporting objects for ..., related to template evolution-2.2-test in | 46 | log> Exporting objects for ..., related to template evolution-2.2-test in |
87 | 46 | 47 | ||
88 | === modified file 'lib/lp/translations/doc/poexport-request.txt' | |||
89 | --- lib/lp/translations/doc/poexport-request.txt 2010-02-19 16:54:42 +0000 | |||
90 | +++ lib/lp/translations/doc/poexport-request.txt 2010-03-06 06:29:39 +0000 | |||
91 | @@ -53,9 +53,13 @@ | |||
92 | 53 | 53 | ||
93 | 54 | Now we request that the queue be processed. | 54 | Now we request that the queue be processed. |
94 | 55 | 55 | ||
96 | 56 | >>> from lp.testing.faketransaction import FakeTransaction | 56 | (Commits are needed to make the test requests seep through to the slave |
97 | 57 | database). | ||
98 | 58 | |||
99 | 59 | >>> import transaction | ||
100 | 57 | >>> from lp.translations.scripts.po_export_queue import process_queue | 60 | >>> from lp.translations.scripts.po_export_queue import process_queue |
102 | 58 | >>> process_queue(FakeTransaction(), MockLogger()) | 61 | >>> transaction.commit() |
103 | 62 | >>> process_queue(transaction, MockLogger()) | ||
104 | 59 | log> Exporting objects for Happy Downloader, related to template pmount | 63 | log> Exporting objects for Happy Downloader, related to template pmount |
105 | 60 | in Ubuntu Hoary package "pmount" | 64 | in Ubuntu Hoary package "pmount" |
106 | 61 | log> Stored file at http://.../launchpad-export.tar.gz | 65 | log> Stored file at http://.../launchpad-export.tar.gz |
107 | @@ -185,7 +189,8 @@ | |||
108 | 185 | >>> from lp.translations.interfaces.translationfileformat import ( | 189 | >>> from lp.translations.interfaces.translationfileformat import ( |
109 | 186 | ... TranslationFileFormat) | 190 | ... TranslationFileFormat) |
110 | 187 | >>> request_set.addRequest(person, None, [cs], TranslationFileFormat.MO) | 191 | >>> request_set.addRequest(person, None, [cs], TranslationFileFormat.MO) |
112 | 188 | >>> process_queue(FakeTransaction(), MockLogger()) | 192 | >>> transaction.commit() |
113 | 193 | >>> process_queue(transaction, MockLogger()) | ||
114 | 189 | log> Exporting objects for Happy Downloader, related to template pmount | 194 | log> Exporting objects for Happy Downloader, related to template pmount |
115 | 190 | in Ubuntu Hoary package "pmount" | 195 | in Ubuntu Hoary package "pmount" |
116 | 191 | log> Stored file at http://.../cs_LC_MESSAGES_pmount.mo | 196 | log> Stored file at http://.../cs_LC_MESSAGES_pmount.mo |
117 | 192 | 197 | ||
118 | === added file 'lib/lp/translations/doc/poexportqueue-replication-lag.txt' | |||
119 | --- lib/lp/translations/doc/poexportqueue-replication-lag.txt 1970-01-01 00:00:00 +0000 | |||
120 | +++ lib/lp/translations/doc/poexportqueue-replication-lag.txt 2010-03-06 06:29:39 +0000 | |||
121 | @@ -0,0 +1,89 @@ | |||
122 | 1 | = Replication Lag and the Export Queue = | ||
123 | 2 | |||
124 | 3 | Due to replication lag it's possible for the export queue to see a | ||
125 | 4 | request on the slave store that it actually just removed from the master | ||
126 | 5 | store. | ||
127 | 6 | |||
128 | 7 | We start our story with an empty export queue. | ||
129 | 8 | |||
130 | 9 | >>> from datetime import timedelta | ||
131 | 10 | >>> import transaction | ||
132 | 11 | >>> from zope.component import getUtility | ||
133 | 12 | >>> from canonical.launchpad.interfaces.lpstorm import IMasterStore | ||
134 | 13 | >>> from lp.translations.interfaces.poexportrequest import ( | ||
135 | 14 | ... IPOExportRequestSet) | ||
136 | 15 | >>> from lp.translations.interfaces.pofile import IPOFile | ||
137 | 16 | >>> from lp.translations.model.poexportrequest import POExportRequest | ||
138 | 17 | >>> query = IMasterStore(POExportRequest).execute( | ||
139 | 18 | ... "DELETE FROM POExportRequest") | ||
140 | 19 | |||
141 | 20 | >>> queue = getUtility(IPOExportRequestSet) | ||
142 | 21 | |||
143 | 22 | We have somebody making an export request. | ||
144 | 23 | |||
145 | 24 | >>> requester = factory.makePersonNoCommit( | ||
146 | 25 | ... email='punter@example.com', name='punter') | ||
147 | 26 | |||
148 | 27 | >>> template1 = factory.makePOTemplate() | ||
149 | 28 | >>> pofile1_be = factory.makePOFile('be', potemplate=template1) | ||
150 | 29 | >>> pofile1_ja = factory.makePOFile('ja', potemplate=template1) | ||
151 | 30 | >>> queue.addRequest(requester, template1, [pofile1_be, pofile1_ja]) | ||
152 | 31 | >>> query = IMasterStore(POExportRequest).execute( | ||
153 | 32 | ... "UPDATE POExportRequest SET date_created = '2010-01-10'::date") | ||
154 | 33 | |||
155 | 34 | Later, a different and separate request follows. | ||
156 | 35 | |||
157 | 36 | >>> template2 = factory.makePOTemplate() | ||
158 | 37 | >>> pofile2_se = factory.makePOFile('se', potemplate=template2) | ||
159 | 38 | >>> pofile2_ga = factory.makePOFile('ga', potemplate=template2) | ||
160 | 39 | >>> queue.addRequest(requester, template2, [pofile2_se, pofile2_ga]) | ||
161 | 40 | |||
162 | 41 | The database is replicated in this state. | ||
163 | 42 | |||
164 | 43 | >>> transaction.commit() | ||
165 | 44 | |||
166 | 45 | getRequest at this point returns the oldest request. | ||
167 | 46 | |||
168 | 47 | >>> def summarize_request(request_tuple): | ||
169 | 48 | ... """Summarize files in export request.""" | ||
170 | 49 | ... person, sources, format, request_ids = request_tuple | ||
171 | 50 | ... summary = [] | ||
172 | 51 | ... for source in sources: | ||
173 | 52 | ... if IPOFile.providedBy(source): | ||
174 | 53 | ... summary.append(source.language.code) | ||
175 | 54 | ... else: | ||
176 | 55 | ... summary.append('(template)') | ||
177 | 56 | ... for entry in sorted(summary): | ||
178 | 57 | ... print entry | ||
179 | 58 | |||
180 | 59 | >>> summarize_request(queue.getRequest()) | ||
181 | 60 | (template) | ||
182 | 61 | be | ||
183 | 62 | ja | ||
184 | 63 | |||
185 | 64 | It doesn't modify the queue, so it'd say the same thing again if we | ||
186 | 65 | were to ask again. | ||
187 | 66 | |||
188 | 67 | >>> repeated_request = queue.getRequest() | ||
189 | 68 | >>> summarize_request(repeated_request) | ||
190 | 69 | (template) | ||
191 | 70 | be | ||
192 | 71 | ja | ||
193 | 72 | |||
194 | 73 | The first request is removed from the master store after processing, but | ||
195 | 74 | not yet from the slave store. (Since this test is all one session, we | ||
196 | 75 | can reproduce this by not committing the removal). The second request | ||
197 | 76 | is still technically on the queue, but no longer "live." | ||
198 | 77 | |||
199 | 78 | >>> person, sources, format, request_ids = repeated_request | ||
200 | 79 | >>> print len(request_ids) | ||
201 | 80 | 3 | ||
202 | 81 | >>> queue.removeRequest(request_ids) | ||
203 | 82 | |||
204 | 83 | In this state, despite the replication lag, getRequest is smart enough | ||
205 | 84 | to return the second request, not the first. | ||
206 | 85 | |||
207 | 86 | >>> summarize_request(queue.getRequest()) | ||
208 | 87 | (template) | ||
209 | 88 | ga | ||
210 | 89 | se | ||
211 | 0 | 90 | ||
212 | === modified file 'lib/lp/translations/doc/potmsgset.txt' | |||
213 | --- lib/lp/translations/doc/potmsgset.txt 2009-10-23 12:44:32 +0000 | |||
214 | +++ lib/lp/translations/doc/potmsgset.txt 2010-03-06 06:29:39 +0000 | |||
215 | @@ -2,10 +2,14 @@ | |||
216 | 2 | 2 | ||
217 | 3 | POTMsgSet represents messages to translate that a POTemplate file has. | 3 | POTMsgSet represents messages to translate that a POTemplate file has. |
218 | 4 | 4 | ||
220 | 5 | We need to get a POTMsgSet object to performe this test. | 5 | In this test we'll be committing a lot to let changes replicate to the |
221 | 6 | slave database. | ||
222 | 7 | |||
223 | 8 | >>> import transaction | ||
224 | 9 | |||
225 | 10 | We need to get a POTMsgSet object to perform this test. | ||
226 | 6 | 11 | ||
227 | 7 | >>> from zope.component import getUtility | 12 | >>> from zope.component import getUtility |
228 | 8 | >>> from canonical.database.sqlbase import flush_database_updates | ||
229 | 9 | >>> from lp.translations.model.translationmessage import ( | 13 | >>> from lp.translations.model.translationmessage import ( |
230 | 10 | ... TranslationMessage) | 14 | ... TranslationMessage) |
231 | 11 | >>> from lp.translations.interfaces.potmsgset import IPOTMsgSet | 15 | >>> from lp.translations.interfaces.potmsgset import IPOTMsgSet |
232 | @@ -734,7 +738,7 @@ | |||
233 | 734 | are not available as suggestions anymore: | 738 | are not available as suggestions anymore: |
234 | 735 | 739 | ||
235 | 736 | >>> evo_distro_template.iscurrent = False | 740 | >>> evo_distro_template.iscurrent = False |
237 | 737 | >>> flush_database_updates() | 741 | >>> transaction.commit() |
238 | 738 | >>> suggestions = ( | 742 | >>> suggestions = ( |
239 | 739 | ... evo_product_message.getExternallyUsedTranslationMessages(spanish)) | 743 | ... evo_product_message.getExternallyUsedTranslationMessages(spanish)) |
240 | 740 | >>> len(suggestions) | 744 | >>> len(suggestions) |
241 | @@ -748,7 +752,7 @@ | |||
242 | 748 | # We set the template as current again so we are sure that we don't show | 752 | # We set the template as current again so we are sure that we don't show |
243 | 749 | # suggestions just due to the change to the official_rosetta flag. | 753 | # suggestions just due to the change to the official_rosetta flag. |
244 | 750 | >>> evo_distro_template.iscurrent = True | 754 | >>> evo_distro_template.iscurrent = True |
246 | 751 | >>> flush_database_updates() | 755 | >>> transaction.commit() |
247 | 752 | >>> suggestions = ( | 756 | >>> suggestions = ( |
248 | 753 | ... evo_product_message.getExternallyUsedTranslationMessages(spanish)) | 757 | ... evo_product_message.getExternallyUsedTranslationMessages(spanish)) |
249 | 754 | >>> len(suggestions) | 758 | >>> len(suggestions) |
250 | @@ -757,7 +761,7 @@ | |||
251 | 757 | And products not using translations officially have the same behaviour. | 761 | And products not using translations officially have the same behaviour. |
252 | 758 | 762 | ||
253 | 759 | >>> evolution.official_rosetta = False | 763 | >>> evolution.official_rosetta = False |
255 | 760 | >>> flush_database_updates() | 764 | >>> transaction.commit() |
256 | 761 | >>> suggestions = evo_distro_message.getExternallyUsedTranslationMessages( | 765 | >>> suggestions = evo_distro_message.getExternallyUsedTranslationMessages( |
257 | 762 | ... spanish) | 766 | ... spanish) |
258 | 763 | >>> len(suggestions) | 767 | >>> len(suggestions) |
259 | @@ -767,7 +771,7 @@ | |||
260 | 767 | 771 | ||
261 | 768 | >>> ubuntu.official_rosetta = True | 772 | >>> ubuntu.official_rosetta = True |
262 | 769 | >>> evolution.official_rosetta = True | 773 | >>> evolution.official_rosetta = True |
264 | 770 | >>> flush_database_updates() | 774 | >>> transaction.commit() |
265 | 771 | 775 | ||
266 | 772 | 776 | ||
267 | 773 | == POTMsgSet.getExternallySuggestedTranslationMessages == | 777 | == POTMsgSet.getExternallySuggestedTranslationMessages == |
268 | @@ -850,7 +854,7 @@ | |||
269 | 850 | we get no suggestions. | 854 | we get no suggestions. |
270 | 851 | 855 | ||
271 | 852 | >>> potmsgset_translated.potemplate.iscurrent = False | 856 | >>> potmsgset_translated.potemplate.iscurrent = False |
273 | 853 | >>> flush_database_updates() | 857 | >>> transaction.commit() |
274 | 854 | 858 | ||
275 | 855 | >>> wiki_submissions = ( | 859 | >>> wiki_submissions = ( |
276 | 856 | ... potmsgset_untranslated.getExternallyUsedTranslationMessages( | 860 | ... potmsgset_untranslated.getExternallyUsedTranslationMessages( |
277 | @@ -865,7 +869,7 @@ | |||
278 | 865 | # suggestions just due to the change to the official_rosetta flag. | 869 | # suggestions just due to the change to the official_rosetta flag. |
279 | 866 | >>> potmsgset_translated.potemplate.iscurrent = True | 870 | >>> potmsgset_translated.potemplate.iscurrent = True |
280 | 867 | >>> ubuntu.official_rosetta = False | 871 | >>> ubuntu.official_rosetta = False |
282 | 868 | >>> flush_database_updates() | 872 | >>> transaction.commit() |
283 | 869 | 873 | ||
284 | 870 | >>> wiki_submissions = ( | 874 | >>> wiki_submissions = ( |
285 | 871 | ... potmsgset_untranslated.getExternallyUsedTranslationMessages( | 875 | ... potmsgset_untranslated.getExternallyUsedTranslationMessages( |
286 | 872 | 876 | ||
287 | === modified file 'lib/lp/translations/interfaces/poexportrequest.py' | |||
288 | --- lib/lp/translations/interfaces/poexportrequest.py 2010-02-19 16:02:16 +0000 | |||
289 | +++ lib/lp/translations/interfaces/poexportrequest.py 2010-03-06 06:29:39 +0000 | |||
290 | @@ -11,7 +11,7 @@ | |||
291 | 11 | ] | 11 | ] |
292 | 12 | 12 | ||
293 | 13 | from zope.interface import Interface | 13 | from zope.interface import Interface |
295 | 14 | from zope.schema import Int, Object | 14 | from zope.schema import Datetime, Int, Object |
296 | 15 | 15 | ||
297 | 16 | from lp.registry.interfaces.person import IPerson | 16 | from lp.registry.interfaces.person import IPerson |
298 | 17 | from lp.translations.interfaces.pofile import IPOFile | 17 | from lp.translations.interfaces.pofile import IPOFile |
299 | @@ -37,12 +37,25 @@ | |||
300 | 37 | :param pofiles: A list of PO files to export. | 37 | :param pofiles: A list of PO files to export. |
301 | 38 | """ | 38 | """ |
302 | 39 | 39 | ||
309 | 40 | def popRequest(): | 40 | def getRequest(): |
310 | 41 | """Take the next request out of the queue. | 41 | """Get the next request from the queue. |
311 | 42 | 42 | ||
312 | 43 | Returns a 3-tuple containing the person who made the request, the PO | 43 | Returns a tuple containing: |
313 | 44 | template the request was for, and a list of `POTemplate` and `POFile` | 44 | * The person who made the request. |
314 | 45 | objects to export. | 45 | * A list of POFiles and/or POTemplates that are to be exported. |
315 | 46 | * The requested `TranslationFileFormat`. | ||
316 | 47 | * The list of request record ids making up this request. | ||
317 | 48 | |||
318 | 49 | The objects are all read-only objects from the slave store. The | ||
319 | 50 | request ids list should be passed to `removeRequest` when | ||
320 | 51 | processing of the request completes. | ||
321 | 52 | """ | ||
322 | 53 | |||
323 | 54 | def removeRequest(self, request_ids): | ||
324 | 55 | """Remove a request off the queue. | ||
325 | 56 | |||
326 | 57 | :param request_ids: A list of request record ids as returned by | ||
327 | 58 | `getRequest`. | ||
328 | 46 | """ | 59 | """ |
329 | 47 | 60 | ||
330 | 48 | 61 | ||
331 | @@ -51,6 +64,9 @@ | |||
332 | 51 | title=u'The person who made the request.', | 64 | title=u'The person who made the request.', |
333 | 52 | required=True, readonly=True, schema=IPerson) | 65 | required=True, readonly=True, schema=IPerson) |
334 | 53 | 66 | ||
335 | 67 | date_created = Datetime( | ||
336 | 68 | title=u"Request's creation timestamp.", required=True, readonly=True) | ||
337 | 69 | |||
338 | 54 | potemplate = Object( | 70 | potemplate = Object( |
339 | 55 | title=u'The translation template to which the requested file belong.', | 71 | title=u'The translation template to which the requested file belong.', |
340 | 56 | required=True, readonly=True, schema=IPOTemplate) | 72 | required=True, readonly=True, schema=IPOTemplate) |
341 | 57 | 73 | ||
342 | === modified file 'lib/lp/translations/interfaces/potmsgset.py' | |||
343 | --- lib/lp/translations/interfaces/potmsgset.py 2009-10-22 15:51:58 +0000 | |||
344 | +++ lib/lp/translations/interfaces/potmsgset.py 2010-03-06 06:29:39 +0000 | |||
345 | @@ -163,19 +163,25 @@ | |||
346 | 163 | """ | 163 | """ |
347 | 164 | 164 | ||
348 | 165 | def getExternallyUsedTranslationMessages(language): | 165 | def getExternallyUsedTranslationMessages(language): |
353 | 166 | """Returns all externally used translations. | 166 | """Find externally used translations for the same message. |
354 | 167 | 167 | ||
355 | 168 | External are those on other templates for the same English message. | 168 | This is used to find suggestions for translating this |
356 | 169 | "Used" messages are either current or imported ones. | 169 | `POTMsgSet` that are actually used (i.e. current or imported) in |
357 | 170 | other templates. | ||
358 | 171 | |||
359 | 172 | The suggestions are read-only; they come from the slave store. | ||
360 | 170 | 173 | ||
361 | 171 | :param language: language we want translations for. | 174 | :param language: language we want translations for. |
362 | 172 | """ | 175 | """ |
363 | 173 | 176 | ||
364 | 174 | def getExternallySuggestedTranslationMessages(language): | 177 | def getExternallySuggestedTranslationMessages(language): |
369 | 175 | """Return all externally suggested translations. | 178 | """Find externally suggested translations for the same message. |
370 | 176 | 179 | ||
371 | 177 | External are those on other templates for the same English message. | 180 | This is used to find suggestions for translating this |
372 | 178 | "Suggested" messages are those which are neither current nor imported. | 181 | `POTMsgSet` that were entered in another context, but for the |
373 | 182 | same English text, and are not in actual use. | ||
374 | 183 | |||
375 | 184 | The suggestions are read-only; they come from the slave store. | ||
376 | 179 | 185 | ||
377 | 180 | :param language: language we want translations for. | 186 | :param language: language we want translations for. |
378 | 181 | """ | 187 | """ |
379 | 182 | 188 | ||
380 | === modified file 'lib/lp/translations/model/poexportrequest.py' | |||
381 | --- lib/lp/translations/model/poexportrequest.py 2010-02-19 17:06:01 +0000 | |||
382 | +++ lib/lp/translations/model/poexportrequest.py 2010-03-06 06:29:39 +0000 | |||
383 | @@ -15,9 +15,12 @@ | |||
384 | 15 | from zope.component import getUtility | 15 | from zope.component import getUtility |
385 | 16 | from zope.interface import implements | 16 | from zope.interface import implements |
386 | 17 | 17 | ||
387 | 18 | from canonical.database.constants import DEFAULT | ||
388 | 19 | from canonical.database.datetimecol import UtcDateTimeCol | ||
389 | 20 | from canonical.database.enumcol import EnumCol | ||
390 | 18 | from canonical.database.sqlbase import quote, SQLBase, sqlvalues | 21 | from canonical.database.sqlbase import quote, SQLBase, sqlvalues |
391 | 19 | from canonical.database.enumcol import EnumCol | ||
392 | 20 | 22 | ||
393 | 23 | from canonical.launchpad.interfaces.lpstorm import IMasterStore, ISlaveStore | ||
394 | 21 | from lp.translations.interfaces.poexportrequest import ( | 24 | from lp.translations.interfaces.poexportrequest import ( |
395 | 22 | IPOExportRequest, IPOExportRequestSet) | 25 | IPOExportRequest, IPOExportRequestSet) |
396 | 23 | from lp.translations.interfaces.potemplate import IPOTemplate | 26 | from lp.translations.interfaces.potemplate import IPOTemplate |
397 | @@ -112,36 +115,69 @@ | |||
398 | 112 | existing.id IS NULL | 115 | existing.id IS NULL |
399 | 113 | """ % query_params) | 116 | """ % query_params) |
400 | 114 | 117 | ||
431 | 115 | def popRequest(self): | 118 | def _getOldestLiveRequest(self): |
432 | 116 | """See `IPOExportRequestSet`.""" | 119 | """Return the oldest live request on the master store. |
433 | 117 | try: | 120 | |
434 | 118 | request = POExportRequest.select(limit=1, orderBy='id')[0] | 121 | Due to replication lag, the master store is always a little |
435 | 119 | except IndexError: | 122 | ahead of the slave store that exports come from. |
436 | 120 | return None, None, None | 123 | """ |
437 | 121 | 124 | master_store = IMasterStore(POExportRequest) | |
438 | 122 | person = request.person | 125 | sorted_by_id = master_store.find(POExportRequest).order_by( |
439 | 123 | format = request.format | 126 | POExportRequest.id) |
440 | 124 | 127 | return sorted_by_id.first() | |
441 | 125 | query = """ | 128 | |
442 | 126 | person = %s AND | 129 | def _getHeadRequest(self): |
443 | 127 | format = %s AND | 130 | """Return oldest request on the queue.""" |
444 | 128 | date_created = ( | 131 | # Due to replication lag, it's possible that the slave store |
445 | 129 | SELECT date_created | 132 | # still has copies of requests that have already been completed |
446 | 130 | FROM POExportRequest | 133 | # and deleted from the master store. So first get the oldest |
447 | 131 | ORDER BY id | 134 | # request that is "live," i.e. still present on the master |
448 | 132 | LIMIT 1)""" % sqlvalues(person, format) | 135 | # store. |
449 | 133 | requests = POExportRequest.select(query, orderBy='potemplate') | 136 | oldest_live = self._getOldestLiveRequest() |
450 | 134 | objects = [] | 137 | if oldest_live is None: |
451 | 135 | 138 | return None | |
452 | 136 | for request in requests: | 139 | else: |
453 | 137 | if request.pofile is not None: | 140 | return ISlaveStore(POExportRequest).find( |
454 | 138 | objects.append(request.pofile) | 141 | POExportRequest, |
455 | 139 | else: | 142 | POExportRequest.id == oldest_live.id).one() |
456 | 140 | objects.append(request.potemplate) | 143 | |
457 | 141 | 144 | def getRequest(self): | |
458 | 142 | POExportRequest.delete(request.id) | 145 | """See `IPOExportRequestSet`.""" |
459 | 143 | 146 | # Exports happen off the slave store. To ensure that export | |
460 | 144 | return person, objects, format | 147 | # does not happen until requests have been replicated to the |
461 | 148 | # slave, they are read primarily from the slave even though they | ||
462 | 149 | # are deleted on the master afterwards. | ||
463 | 150 | head = self._getHeadRequest() | ||
464 | 151 | if head is None: | ||
465 | 152 | return None, None, None, None | ||
466 | 153 | |||
467 | 154 | requests = ISlaveStore(POExportRequest).find( | ||
468 | 155 | POExportRequest, | ||
469 | 156 | POExportRequest.person == head.person, | ||
470 | 157 | POExportRequest.format == head.format, | ||
471 | 158 | POExportRequest.date_created == head.date_created).order_by( | ||
472 | 159 | POExportRequest.potemplateID) | ||
473 | 160 | |||
474 | 161 | summary = [ | ||
475 | 162 | (request.id, request.pofile or request.potemplate) | ||
476 | 163 | for request in requests | ||
477 | 164 | ] | ||
478 | 165 | |||
479 | 166 | sources = [source for request_id, source in summary] | ||
480 | 167 | request_ids = [request_id for request_id, source in summary] | ||
481 | 168 | |||
482 | 169 | return head.person, sources, head.format, request_ids | ||
483 | 170 | |||
484 | 171 | def removeRequest(self, request_ids): | ||
485 | 172 | """See `IPOExportRequestSet`.""" | ||
486 | 173 | if len(request_ids) > 0: | ||
487 | 174 | # Storm 0.15 does not have direct support for deleting based | ||
488 | 175 | # on is_in expressions and such, so do it the hard way. | ||
489 | 176 | ids_string = ', '.join(sqlvalues(*request_ids)) | ||
490 | 177 | IMasterStore(POExportRequest).execute(""" | ||
491 | 178 | DELETE FROM POExportRequest | ||
492 | 179 | WHERE id in (%s) | ||
493 | 180 | """ % ids_string) | ||
494 | 145 | 181 | ||
495 | 146 | 182 | ||
496 | 147 | class POExportRequest(SQLBase): | 183 | class POExportRequest(SQLBase): |
497 | @@ -152,6 +188,7 @@ | |||
498 | 152 | person = ForeignKey( | 188 | person = ForeignKey( |
499 | 153 | dbName='person', foreignKey='Person', | 189 | dbName='person', foreignKey='Person', |
500 | 154 | storm_validator=validate_public_person, notNull=True) | 190 | storm_validator=validate_public_person, notNull=True) |
501 | 191 | date_created = UtcDateTimeCol(dbName='date_created', default=DEFAULT) | ||
502 | 155 | potemplate = ForeignKey(dbName='potemplate', foreignKey='POTemplate', | 192 | potemplate = ForeignKey(dbName='potemplate', foreignKey='POTemplate', |
503 | 156 | notNull=True) | 193 | notNull=True) |
504 | 157 | pofile = ForeignKey(dbName='pofile', foreignKey='POFile') | 194 | pofile = ForeignKey(dbName='pofile', foreignKey='POFile') |
505 | 158 | 195 | ||
506 | === modified file 'lib/lp/translations/model/potmsgset.py' | |||
507 | --- lib/lp/translations/model/potmsgset.py 2010-01-28 09:53:45 +0000 | |||
508 | +++ lib/lp/translations/model/potmsgset.py 2010-03-06 06:29:39 +0000 | |||
509 | @@ -15,6 +15,7 @@ | |||
510 | 15 | from zope.component import getUtility | 15 | from zope.component import getUtility |
511 | 16 | 16 | ||
512 | 17 | from sqlobject import ForeignKey, IntCol, StringCol, SQLObjectNotFound | 17 | from sqlobject import ForeignKey, IntCol, StringCol, SQLObjectNotFound |
513 | 18 | from storm.expr import SQL | ||
514 | 18 | from storm.store import EmptyResultSet, Store | 19 | from storm.store import EmptyResultSet, Store |
515 | 19 | 20 | ||
516 | 20 | from canonical.config import config | 21 | from canonical.config import config |
517 | @@ -27,6 +28,7 @@ | |||
518 | 27 | from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities | 28 | from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities |
519 | 28 | from canonical.launchpad.readonly import is_read_only | 29 | from canonical.launchpad.readonly import is_read_only |
520 | 29 | from canonical.launchpad.webapp.interfaces import UnexpectedFormData | 30 | from canonical.launchpad.webapp.interfaces import UnexpectedFormData |
521 | 31 | from canonical.launchpad.interfaces.lpstorm import ISlaveStore | ||
522 | 30 | from lp.translations.interfaces.pofile import IPOFileSet | 32 | from lp.translations.interfaces.pofile import IPOFileSet |
523 | 31 | from lp.translations.interfaces.potmsgset import ( | 33 | from lp.translations.interfaces.potmsgset import ( |
524 | 32 | BrokenTextError, | 34 | BrokenTextError, |
525 | @@ -324,6 +326,9 @@ | |||
526 | 324 | 326 | ||
527 | 325 | A message is used if it's either imported or current, and unused | 327 | A message is used if it's either imported or current, and unused |
528 | 326 | otherwise. | 328 | otherwise. |
529 | 329 | |||
530 | 330 | Suggestions are read-only, so these objects come from the slave | ||
531 | 331 | store. | ||
532 | 327 | """ | 332 | """ |
533 | 328 | if not config.rosetta.global_suggestions_enabled: | 333 | if not config.rosetta.global_suggestions_enabled: |
534 | 329 | return [] | 334 | return [] |
535 | @@ -387,7 +392,9 @@ | |||
536 | 387 | ORDER BY %(msgstrs)s, date_created DESC | 392 | ORDER BY %(msgstrs)s, date_created DESC |
537 | 388 | ''' % ids_query_params | 393 | ''' % ids_query_params |
538 | 389 | 394 | ||
540 | 390 | result = TranslationMessage.select('id IN (%s)' % ids_query) | 395 | result = ISlaveStore(TranslationMessage).find( |
541 | 396 | TranslationMessage, | ||
542 | 397 | TranslationMessage.id.is_in(SQL(ids_query))) | ||
543 | 391 | 398 | ||
544 | 392 | return shortlist(result, longest_expected=100, hardlimit=2000) | 399 | return shortlist(result, longest_expected=100, hardlimit=2000) |
545 | 393 | 400 | ||
546 | 394 | 401 | ||
547 | === modified file 'lib/lp/translations/scripts/po_export_queue.py' | |||
548 | --- lib/lp/translations/scripts/po_export_queue.py 2009-08-17 13:42:00 +0000 | |||
549 | +++ lib/lp/translations/scripts/po_export_queue.py 2010-03-06 06:29:39 +0000 | |||
550 | @@ -373,32 +373,18 @@ | |||
551 | 373 | 373 | ||
552 | 374 | 374 | ||
553 | 375 | def process_queue(transaction_manager, logger): | 375 | def process_queue(transaction_manager, logger): |
555 | 376 | """Process each request in the translation export queue. | 376 | """Process all requests in the translation export queue. |
556 | 377 | 377 | ||
559 | 378 | Each item is removed from the queue as it is processed, we only handle | 378 | Each item is removed from the queue as it is processed. |
558 | 379 | one request with each function call. | ||
560 | 380 | """ | 379 | """ |
561 | 381 | request_set = getUtility(IPOExportRequestSet) | 380 | request_set = getUtility(IPOExportRequestSet) |
573 | 382 | 381 | no_request = (None, None, None, None) | |
574 | 383 | request = request_set.popRequest() | 382 | |
575 | 384 | 383 | request = request_set.getRequest() | |
576 | 385 | if None in request: | 384 | while request != no_request: |
577 | 386 | # Any value is None and we must have all values as not None to have | 385 | person, objects, format, request_ids = request |
567 | 387 | # something to process... | ||
568 | 388 | return | ||
569 | 389 | |||
570 | 390 | person, objects, format = request | ||
571 | 391 | |||
572 | 392 | try: | ||
578 | 393 | process_request(person, objects, format, logger) | 386 | process_request(person, objects, format, logger) |
589 | 394 | except psycopg2.Error: | 387 | request_set.removeRequest(request_ids) |
580 | 395 | # We had a DB error, we don't try to recover it here, just exit | ||
581 | 396 | # from the script and next run will retry the export. | ||
582 | 397 | logger.error( | ||
583 | 398 | "A DB exception was raised when exporting files for %s" % ( | ||
584 | 399 | person.displayname), | ||
585 | 400 | exc_info=True) | ||
586 | 401 | transaction_manager.abort() | ||
587 | 402 | else: | ||
588 | 403 | # Apply all changes. | ||
590 | 404 | transaction_manager.commit() | 388 | transaction_manager.commit() |
591 | 389 | |||
592 | 390 | request = request_set.getRequest() | ||
593 | 405 | 391 | ||
594 | === modified file 'lib/lp/translations/tests/test_potmsgset.py' | |||
595 | --- lib/lp/translations/tests/test_potmsgset.py 2009-11-25 15:08:26 +0000 | |||
596 | +++ lib/lp/translations/tests/test_potmsgset.py 2010-03-06 06:29:39 +0000 | |||
597 | @@ -1,4 +1,4 @@ | |||
599 | 1 | # Copyright 2009 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2009-2010 Canonical Ltd. This software is licensed under the |
600 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
601 | 3 | 3 | ||
602 | 4 | # pylint: disable-msg=C0102 | 4 | # pylint: disable-msg=C0102 |
603 | @@ -329,6 +329,8 @@ | |||
604 | 329 | external_pofile = self.factory.makePOFile('sr', external_template) | 329 | external_pofile = self.factory.makePOFile('sr', external_template) |
605 | 330 | serbian = external_pofile.language | 330 | serbian = external_pofile.language |
606 | 331 | 331 | ||
607 | 332 | transaction.commit() | ||
608 | 333 | |||
609 | 332 | # When there is no translation for the external POTMsgSet, | 334 | # When there is no translation for the external POTMsgSet, |
610 | 333 | # no externally used suggestions are returned. | 335 | # no externally used suggestions are returned. |
611 | 334 | self.assertEquals( | 336 | self.assertEquals( |
612 | @@ -340,6 +342,9 @@ | |||
613 | 340 | external_suggestion = self.factory.makeSharedTranslationMessage( | 342 | external_suggestion = self.factory.makeSharedTranslationMessage( |
614 | 341 | pofile=external_pofile, potmsgset=external_potmsgset, | 343 | pofile=external_pofile, potmsgset=external_potmsgset, |
615 | 342 | suggestion=True) | 344 | suggestion=True) |
616 | 345 | |||
617 | 346 | transaction.commit() | ||
618 | 347 | |||
619 | 343 | self.assertEquals( | 348 | self.assertEquals( |
620 | 344 | self.potmsgset.getExternallyUsedTranslationMessages(serbian), | 349 | self.potmsgset.getExternallyUsedTranslationMessages(serbian), |
621 | 345 | []) | 350 | []) |
622 | @@ -350,6 +355,9 @@ | |||
623 | 350 | pofile=external_pofile, potmsgset=external_potmsgset, | 355 | pofile=external_pofile, potmsgset=external_potmsgset, |
624 | 351 | suggestion=False, is_imported=True) | 356 | suggestion=False, is_imported=True) |
625 | 352 | imported_translation.is_current = False | 357 | imported_translation.is_current = False |
626 | 358 | |||
627 | 359 | transaction.commit() | ||
628 | 360 | |||
629 | 353 | self.assertEquals( | 361 | self.assertEquals( |
630 | 354 | self.potmsgset.getExternallyUsedTranslationMessages(serbian), | 362 | self.potmsgset.getExternallyUsedTranslationMessages(serbian), |
631 | 355 | [imported_translation]) | 363 | [imported_translation]) |
632 | @@ -359,6 +367,9 @@ | |||
633 | 359 | current_translation = self.factory.makeSharedTranslationMessage( | 367 | current_translation = self.factory.makeSharedTranslationMessage( |
634 | 360 | pofile=external_pofile, potmsgset=external_potmsgset, | 368 | pofile=external_pofile, potmsgset=external_potmsgset, |
635 | 361 | suggestion=False, is_imported=False) | 369 | suggestion=False, is_imported=False) |
636 | 370 | |||
637 | 371 | transaction.commit() | ||
638 | 372 | |||
639 | 362 | self.assertEquals( | 373 | self.assertEquals( |
640 | 363 | self.potmsgset.getExternallyUsedTranslationMessages(serbian), | 374 | self.potmsgset.getExternallyUsedTranslationMessages(serbian), |
641 | 364 | [imported_translation, current_translation]) | 375 | [imported_translation, current_translation]) |
642 | @@ -377,6 +388,8 @@ | |||
643 | 377 | external_pofile = self.factory.makePOFile('sr', external_template) | 388 | external_pofile = self.factory.makePOFile('sr', external_template) |
644 | 378 | serbian = external_pofile.language | 389 | serbian = external_pofile.language |
645 | 379 | 390 | ||
646 | 391 | transaction.commit() | ||
647 | 392 | |||
648 | 380 | # When there is no translation for the external POTMsgSet, | 393 | # When there is no translation for the external POTMsgSet, |
649 | 381 | # no externally used suggestions are returned. | 394 | # no externally used suggestions are returned. |
650 | 382 | self.assertEquals( | 395 | self.assertEquals( |
651 | @@ -388,6 +401,9 @@ | |||
652 | 388 | external_suggestion = self.factory.makeSharedTranslationMessage( | 401 | external_suggestion = self.factory.makeSharedTranslationMessage( |
653 | 389 | pofile=external_pofile, potmsgset=external_potmsgset, | 402 | pofile=external_pofile, potmsgset=external_potmsgset, |
654 | 390 | suggestion=True) | 403 | suggestion=True) |
655 | 404 | |||
656 | 405 | transaction.commit() | ||
657 | 406 | |||
658 | 391 | self.assertEquals( | 407 | self.assertEquals( |
659 | 392 | self.potmsgset.getExternallySuggestedTranslationMessages(serbian), | 408 | self.potmsgset.getExternallySuggestedTranslationMessages(serbian), |
660 | 393 | [external_suggestion]) | 409 | [external_suggestion]) |
661 | @@ -398,6 +414,9 @@ | |||
662 | 398 | pofile=external_pofile, potmsgset=external_potmsgset, | 414 | pofile=external_pofile, potmsgset=external_potmsgset, |
663 | 399 | suggestion=False, is_imported=True) | 415 | suggestion=False, is_imported=True) |
664 | 400 | imported_translation.is_current = False | 416 | imported_translation.is_current = False |
665 | 417 | |||
666 | 418 | transaction.commit() | ||
667 | 419 | |||
668 | 401 | self.assertEquals( | 420 | self.assertEquals( |
669 | 402 | self.potmsgset.getExternallySuggestedTranslationMessages(serbian), | 421 | self.potmsgset.getExternallySuggestedTranslationMessages(serbian), |
670 | 403 | [external_suggestion]) | 422 | [external_suggestion]) |
671 | @@ -407,6 +426,9 @@ | |||
672 | 407 | current_translation = self.factory.makeSharedTranslationMessage( | 426 | current_translation = self.factory.makeSharedTranslationMessage( |
673 | 408 | pofile=external_pofile, potmsgset=external_potmsgset, | 427 | pofile=external_pofile, potmsgset=external_potmsgset, |
674 | 409 | suggestion=False, is_imported=False) | 428 | suggestion=False, is_imported=False) |
675 | 429 | |||
676 | 430 | transaction.commit() | ||
677 | 431 | |||
678 | 410 | self.assertEquals( | 432 | self.assertEquals( |
679 | 411 | self.potmsgset.getExternallySuggestedTranslationMessages(serbian), | 433 | self.potmsgset.getExternallySuggestedTranslationMessages(serbian), |
680 | 412 | [external_suggestion]) | 434 | [external_suggestion]) |
681 | 413 | 435 | ||
682 | === modified file 'lib/lp/translations/tests/test_suggestions.py' | |||
683 | --- lib/lp/translations/tests/test_suggestions.py 2009-07-17 00:26:05 +0000 | |||
684 | +++ lib/lp/translations/tests/test_suggestions.py 2010-03-06 06:29:39 +0000 | |||
685 | @@ -1,10 +1,11 @@ | |||
687 | 1 | # Copyright 2009 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2009-2010 Canonical Ltd. This software is licensed under the |
688 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
689 | 3 | 3 | ||
690 | 4 | __metaclass__ = type | 4 | __metaclass__ = type |
691 | 5 | 5 | ||
692 | 6 | from datetime import datetime, timedelta | 6 | from datetime import datetime, timedelta |
693 | 7 | from pytz import timezone | 7 | from pytz import timezone |
694 | 8 | import transaction | ||
695 | 8 | import unittest | 9 | import unittest |
696 | 9 | 10 | ||
697 | 10 | import gettextpo | 11 | import gettextpo |
698 | @@ -66,6 +67,8 @@ | |||
699 | 66 | ["foutmelding 936"], is_imported=False, | 67 | ["foutmelding 936"], is_imported=False, |
700 | 67 | lock_timestamp=None) | 68 | lock_timestamp=None) |
701 | 68 | 69 | ||
702 | 70 | transaction.commit() | ||
703 | 71 | |||
704 | 69 | used_suggestions = foomsg.getExternallyUsedTranslationMessages( | 72 | used_suggestions = foomsg.getExternallyUsedTranslationMessages( |
705 | 70 | self.nl) | 73 | self.nl) |
706 | 71 | other_suggestions = foomsg.getExternallySuggestedTranslationMessages( | 74 | other_suggestions = foomsg.getExternallySuggestedTranslationMessages( |
707 | @@ -88,6 +91,8 @@ | |||
708 | 88 | ["foutmelding 936"], is_imported=False, | 91 | ["foutmelding 936"], is_imported=False, |
709 | 89 | lock_timestamp=None) | 92 | lock_timestamp=None) |
710 | 90 | 93 | ||
711 | 94 | transaction.commit() | ||
712 | 95 | |||
713 | 91 | # There is a global (externally used) suggestion. | 96 | # There is a global (externally used) suggestion. |
714 | 92 | used_suggestions = foomsg.getExternallyUsedTranslationMessages( | 97 | used_suggestions = foomsg.getExternallyUsedTranslationMessages( |
715 | 93 | self.nl) | 98 | self.nl) |
716 | @@ -117,6 +122,8 @@ | |||
717 | 117 | is_imported=False, lock_timestamp=None) | 122 | is_imported=False, lock_timestamp=None) |
718 | 118 | suggestion.is_current = False | 123 | suggestion.is_current = False |
719 | 119 | 124 | ||
720 | 125 | transaction.commit() | ||
721 | 126 | |||
722 | 120 | used_suggestions = foomsg.getExternallyUsedTranslationMessages( | 127 | used_suggestions = foomsg.getExternallyUsedTranslationMessages( |
723 | 121 | self.nl) | 128 | self.nl) |
724 | 122 | other_suggestions = foomsg.getExternallySuggestedTranslationMessages( | 129 | other_suggestions = foomsg.getExternallySuggestedTranslationMessages( |
725 | 123 | 130 | ||
726 | === modified file 'lib/lp/translations/tests/test_translatablemessage.py' | |||
727 | --- lib/lp/translations/tests/test_translatablemessage.py 2009-08-26 16:24:02 +0000 | |||
728 | +++ lib/lp/translations/tests/test_translatablemessage.py 2010-03-06 06:29:39 +0000 | |||
729 | @@ -1,4 +1,4 @@ | |||
731 | 1 | # Copyright 2009 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2009-2010 Canonical Ltd. This software is licensed under the |
732 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
733 | 3 | 3 | ||
734 | 4 | """Unit tests for `TranslatableMessage`.""" | 4 | """Unit tests for `TranslatableMessage`.""" |
735 | @@ -7,6 +7,7 @@ | |||
736 | 7 | 7 | ||
737 | 8 | from datetime import datetime, timedelta | 8 | from datetime import datetime, timedelta |
738 | 9 | import pytz | 9 | import pytz |
739 | 10 | import transaction | ||
740 | 10 | from unittest import TestLoader | 11 | from unittest import TestLoader |
741 | 11 | 12 | ||
742 | 12 | from lp.testing import TestCaseWithFactory | 13 | from lp.testing import TestCaseWithFactory |
743 | @@ -147,10 +148,12 @@ | |||
744 | 147 | self.message = TranslatableMessage(self.potmsgset, self.pofile) | 148 | self.message = TranslatableMessage(self.potmsgset, self.pofile) |
745 | 148 | 149 | ||
746 | 149 | def test_getExternalTranslations(self): | 150 | def test_getExternalTranslations(self): |
747 | 151 | transaction.commit() | ||
748 | 150 | externals = self.message.getExternalTranslations() | 152 | externals = self.message.getExternalTranslations() |
749 | 151 | self.assertContentEqual([self.external_current], externals) | 153 | self.assertContentEqual([self.external_current], externals) |
750 | 152 | 154 | ||
751 | 153 | def test_getExternalSuggestions(self): | 155 | def test_getExternalSuggestions(self): |
752 | 156 | transaction.commit() | ||
753 | 154 | externals = self.message.getExternalSuggestions() | 157 | externals = self.message.getExternalSuggestions() |
754 | 155 | self.assertContentEqual([self.external_suggestion], externals) | 158 | self.assertContentEqual([self.external_suggestion], externals) |
755 | 156 | 159 |
Hi Jeroen,
This was not a trivial one but your cover letter did a very good job at
explaining what you did. Thanks for taking the time to write it.
I have a few comments/ suggestions below, but r=me
review approve
On Fri, 2010-03-05 at 17:37 +0000, Jeroen T. Vermeulen wrote:
> Jeroen T. Vermeulen has proposed merging lp:~jtv/launchpad/bug-286237 into lp:launchpad/devel.
> === modified file 'lib/lp/ translations/ browser/ tests/test_ baseexportview. py' translations/ browser/ tests/test_ baseexportview. py 2010-02-19 17:06:01 +0000 translations/ browser/ tests/test_ baseexportview. py 2010-03-05 17:38:34 +0000 launchpad. interfaces. lpstorm import IMasterStore launchpad. webapp. servers import LaunchpadTestRe quest eLayer .browser. sourcepackage import ( anslationsExpor tView) .interfaces. translationfile format import ( Format) .model. poexportrequest import POExportRequest POExportRequest ).execute(
> --- lib/lp/
> +++ lib/lp/
> @@ -7,6 +7,7 @@
> import transaction
> import unittest
>
> +from canonical.
> from canonical.
> from canonical.testing import ZopelessDatabas
> from lp.translations
> @@ -15,13 +16,14 @@
> ProductSeriesTr
> from lp.translations
> TranslationFile
> +from lp.translations
> from lp.testing import TestCaseWithFactory
>
>
> def wipe_queue(queue):
> """Erase all export queue entries."""
> - while queue.entry_count > 0:
> - queue.popRequest()
> + queue_ids = IMasterStore(
You don't need to store the queue ids here, do you?
> + "DELETE FROM POExportRequest") ixin(TestCaseWi thFactory) : translations/ doc/poexport- queue.txt' translations/ doc/poexport- queue.txt 2010-02-19 16:02:16 +0000 translations/ doc/poexport- queue.txt 2010-03-05 17:38:34 +0000 queue(fake_ transaction, logging. getLogger( )) commit( ) queue(transacti on, logging. getLogger( )) .../po_ evolution- 2.2.pot request_ set.entry_ count request_ set.addRequest( TranslationFile Format. PO) queue(fake_ transaction, logging. getLogger( )) commit( ) queue(transacti on, logging. getLogger( )) ...eo.po commit( ) librarian_ file(). read() request_ set.addRequest( TranslationFile Format. POCHANGED) queue(fake_ transaction, logging. getLogger( )) commit( ) queue(transacti on, logging. getLogger( )) ...eo.po commit( ) librarian_ file(). read()
>
>
> class BaseExportViewM
>
> === modified file 'lib/lp/
> --- lib/lp/
> +++ lib/lp/
> @@ -275,7 +275,8 @@
>
> Once the queue is processed, the queue is empty again.
>
> - >>> process_
> + >>> transaction.
> + >>> process_
> INFO:...Stored file at http://
>
> >>> export_
> @@ -333,10 +334,10 @@
>
> >>> export_
> ... carlos, pofiles=[pofile], format=
> - >>> process_
> + >>> transaction.
> + >>> process_
> INFO:root:Stored file at http://
>
> - >>> transaction.
> >>> print get_newest_
> # Esperanto translation for ...
> ...
> @@ -354,10 +355,10 @@
>
> >>> export_
> ... carlos, pofiles=[pofile], format=
> - >>> process_
> + >>> transaction.
> + >>> process_
> INFO:root:Stored file at http://
>
> - >>> transaction.
> >>> print get_newest_
> # IMPORTANT: This file does NOT contain a complete PO file structure.
> # DO NOT attempt to import this file b...