Merge lp:~jtv/launchpad/bug-994410-stop-using-latest_message into lp:launchpad

Proposed by Jeroen T. Vermeulen
Status: Merged
Approved by: Jeroen T. Vermeulen
Approved revision: no longer in the source branch.
Merged at revision: 15218
Proposed branch: lp:~jtv/launchpad/bug-994410-stop-using-latest_message
Merge into: lp:launchpad
Diff against target: 424 lines (+75/-202)
7 files modified
lib/lp/translations/doc/translationmessage-destroy.txt (+4/-11)
lib/lp/translations/interfaces/pofiletranslator.py (+6/-10)
lib/lp/translations/model/pofiletranslator.py (+7/-15)
lib/lp/translations/tests/pofiletranslator.txt (+0/-99)
lib/lp/translations/tests/test_pofiletranslator.py (+54/-8)
lib/lp/translations/tests/test_translationmerger.py (+0/-55)
lib/lp/translations/translationmerger.py (+4/-4)
To merge this branch: bzr merge lp:~jtv/launchpad/bug-994410-stop-using-latest_message
Reviewer Review Type Date Requested Status
Stuart Bishop (community) Approve
Review via email: mp+104703@code.launchpad.net

Commit message

Stop using POFileTranslator.latest_message.

Description of the change

This field in POFileTranslator can be costly to compute, and can restrict the way we move records around in scripts, but it's only used in one place and not a critical one at that: the translations-merging script tries to fold POFileTranslator records together when merging translations. An approximation of the correct move is sort of good enough. The POFileTranslator table isn't very exact.

More precision for POFileTranslator would be nice, but if that's what we want, we have much bigger fish to fry. Such as the breakage of the Ubuntu translations-copying script, which we risk exactly because of the need to maintain POFileTranslator, and which in a twist of irony caused us to lose a bunch of records in the Precise opening. We should maintain its integrity asynchronously with a scrubber script later (so that it's still inexact but at least *eventually* consistent) and drop the database trigger and other hoop-jumping that we do to keep this table in shape.

This branch contains no lint (you may notice I removed some), it updates copyright notices, and it passes all translations tests.

Jeroen

To post a comment you must log in.
Revision history for this message
Stuart Bishop (stub) wrote :

Looks fine.

review: Approve
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

Came through EC2 without problems. Now trying to make it land.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/translations/doc/translationmessage-destroy.txt'
2--- lib/lp/translations/doc/translationmessage-destroy.txt 2012-01-20 15:42:44 +0000
3+++ lib/lp/translations/doc/translationmessage-destroy.txt 2012-05-08 04:45:29 +0000
4@@ -33,8 +33,8 @@
5 In two sharing POTemplates with one shared POTMsgSet with one shared
6 translation, we get two POFileTranslator records for each of the POFiles.
7
8- >>> # We need to be able to create persons and projects so let's just use
9- >>> # a global 'postgres' permission which allows everything.
10+ # We need to be able to create persons and projects so let's just use
11+ # a global 'postgres' permission which allows everything.
12 >>> switch_dbuser('postgres')
13 >>> from lp.services.database.sqlbase import sqlvalues
14 >>> from lp.app.enums import ServiceUsage
15@@ -63,13 +63,6 @@
16 ... potmsgset=potmsgset,
17 ... translations=[u"blah"])
18 >>> print POFileTranslator.select(
19- ... "latest_message=%s" % sqlvalues(tm)).count()
20+ ... "pofile IN (%s, %s)"
21+ ... % sqlvalues(devel_sr_pofile, stable_sr_pofile)).count()
22 2
23-
24-Removing a translation message triggers POFileTranslator row removal as well.
25-
26- >>> tm.destroySelf()
27- >>> print list(POFileTranslator.select(
28- ... "latest_message=%s" % sqlvalues(tm)))
29- []
30-
31
32=== modified file 'lib/lp/translations/interfaces/pofiletranslator.py'
33--- lib/lp/translations/interfaces/pofiletranslator.py 2011-12-24 16:54:44 +0000
34+++ lib/lp/translations/interfaces/pofiletranslator.py 2012-05-08 04:45:29 +0000
35@@ -1,4 +1,4 @@
36-# Copyright 2009 Canonical Ltd. This software is licensed under the
37+# Copyright 2009-2012 Canonical Ltd. This software is licensed under the
38 # GNU Affero General Public License version 3 (see the file LICENSE).
39
40 # pylint: disable-msg=E0213
41@@ -21,7 +21,6 @@
42 from lp import _
43 from lp.registry.interfaces.person import IPerson
44 from lp.translations.interfaces.pofile import IPOFile
45-from lp.translations.interfaces.translationmessage import ITranslationMessage
46
47
48 class IPOFileTranslator(Interface):
49@@ -37,10 +36,6 @@
50 title=_(u"The `POFile` modified by the translator."), required=True,
51 readonly=True, schema=IPOFile)
52
53- latest_message = Object(
54- title=_(u"Latest `TranslationMessage` for this person and file."),
55- required=True, readonly=True, schema=ITranslationMessage)
56-
57 date_last_touched = Datetime(
58 title=_(u"When the latest translation message was added."),
59 required=True, readonly=True)
60@@ -69,9 +64,10 @@
61 person and pofile, or None.
62 """
63
64- def getForPOTMsgSet(potmsgset):
65- """Retrieve `POFileTranslator`s for translations of `potmsgset`.
66+ def getForTemplate(potemplate):
67+ """Retrieve `POFileTranslator` objects associated iwth `POTemplate`.
68
69- :return: a query result of `POFileTranslator`s whose
70- `latest_message` are translations of `potmsgset`.
71+ :param potemplate: `POTemplate` to look for.
72+ :return: Result set of `POFileTranslator` records associated with
73+ `POFile`s that translate `potemplate`.
74 """
75
76=== modified file 'lib/lp/translations/model/pofiletranslator.py'
77--- lib/lp/translations/model/pofiletranslator.py 2011-12-30 06:14:56 +0000
78+++ lib/lp/translations/model/pofiletranslator.py 2012-05-08 04:45:29 +0000
79@@ -1,4 +1,4 @@
80-# Copyright 2009 Canonical Ltd. This software is licensed under the
81+# Copyright 2009-2012 Canonical Ltd. This software is licensed under the
82 # GNU Affero General Public License version 3 (see the file LICENSE).
83
84 __metaclass__ = type
85@@ -23,7 +23,7 @@
86 IPOFileTranslator,
87 IPOFileTranslatorSet,
88 )
89-from lp.translations.model.translationmessage import TranslationMessage
90+from lp.translations.model.pofile import POFile
91
92
93 class POFileTranslator(SQLBase):
94@@ -34,9 +34,6 @@
95 person = ForeignKey(
96 dbName='person', foreignKey='Person',
97 storm_validator=validate_public_person, notNull=True)
98- latest_message = ForeignKey(
99- foreignKey='TranslationMessage', dbName='latest_message',
100- notNull=True)
101 date_last_touched = UtcDateTimeCol(
102 dbName='date_last_touched', notNull=False, default=None)
103
104@@ -62,10 +59,6 @@
105 'pofile.potemplate.productseries.product',
106 'pofile.potemplate.distroseries',
107 'pofile.potemplate.sourcepackagename',
108- 'latest_message',
109- 'latest_message.potmsgset',
110- 'latest_message.potmsgset.msgid_singular',
111- 'latest_message.msgstr0',
112 ]))
113
114 def getForPersonPOFile(self, person, pofile):
115@@ -74,10 +67,9 @@
116 POFileTranslator.person == person.id,
117 POFileTranslator.pofile == pofile.id)).one()
118
119- def getForPOTMsgSet(self, potmsgset):
120+ def getForTemplate(self, potemplate):
121 """See `IPOFileTranslatorSet`."""
122- store = Store.of(potmsgset)
123- match = And(
124- POFileTranslator.latest_message == TranslationMessage.id,
125- TranslationMessage.potmsgset == potmsgset)
126- return store.find(POFileTranslator, match).config(distinct=True)
127+ return Store.of(potemplate).find(
128+ POFileTranslator,
129+ POFileTranslator.pofileID == POFile.id,
130+ POFile.potemplateID == potemplate.id)
131
132=== removed file 'lib/lp/translations/tests/pofiletranslator.txt'
133--- lib/lp/translations/tests/pofiletranslator.txt 2011-12-30 06:14:56 +0000
134+++ lib/lp/translations/tests/pofiletranslator.txt 1970-01-01 00:00:00 +0000
135@@ -1,99 +0,0 @@
136-The POFileTranslator table is an eager materialized view, containing
137-references to the most recent posubmission made by a particular person
138-to a particular pofile.
139-
140-This table is read only to most users, but for this test we connect
141-as a superuser so we can poke at it directly.
142-
143- >>> from lp.services.database.sqlbase import connect
144- >>> connection = connect(user='testadmin')
145- >>> cur = connection.cursor()
146- >>> def pofiletranslator(person_id, pofile_id):
147- ... cur.execute("""
148- ... SELECT latest_message, date_last_touched
149- ... FROM POFileTranslator
150- ... WHERE person = %(person_id)s AND pofile = %(pofile_id)s
151- ... """, vars())
152- ... result = cur.fetchone()
153- ... if result is None:
154- ... return None
155- ... return list(result)
156-
157- >>> stub_id = 22
158- >>> pofile_id = 1
159- >>> language_id = 387
160- >>> potmsgset_id = 1
161-
162-Note that our oldest TranslationMessage belongs to pofile #1.
163-
164-Stub has so far not translated anything in this pofile
165-
166- >>> pofiletranslator(stub_id, pofile_id) is None
167- True
168-
169-If we add a message, the cache is updated
170-
171- >>> cur.execute("""
172- ... INSERT INTO TranslationMessage(
173- ... potmsgset, msgstr1, origin, submitter, language
174- ... ) VALUES (
175- ... %(potmsgset_id)s, 1, 1, %(stub_id)s, %(language_id)s)
176- ... """, vars())
177- >>> posubmission_id, date_touched = pofiletranslator(stub_id, pofile_id)
178-
179-
180-We now set the last touched timestamp into the past so we can detect that
181-it is correctly updated. This is only possible because we are connected to
182-the database as the testadmin user.
183-
184- >>> cur.execute("""
185- ... UPDATE POFileTranslator
186- ... SET date_last_touched = CURRENT_TIMESTAMP - '1 day'::interval
187- ... WHERE person=%(stub_id)s AND pofile=%(pofile_id)s
188- ... """, vars())
189-
190-
191-If we add a new submission to that pofile, the cache is updated including
192-the last touched timestamp.
193-
194- >>> cur.execute("""
195- ... INSERT INTO TranslationMessage(
196- ... potmsgset, msgstr1, origin, submitter, language
197- ... ) VALUES (
198- ... %(potmsgset_id)s, 2, 1, %(stub_id)s, %(language_id)s)
199- ... """, vars())
200- >>> new_posubmission_id, new_date_touched = pofiletranslator(
201- ... stub_id, pofile_id)
202- >>> posubmission_id == new_posubmission_id
203- False
204-
205-
206-We should get the same timestamp as before, despite having updated the
207-cache manually, as we are in the same database transaction and
208-CURRENT_TIMESTAMP will return a constant value throughout the transaction
209-and the triggers will have reset the value in the cache.
210-
211- >>> date_touched == new_date_touched
212- True
213-
214-
215-If we update the submissin, the cache is updated
216-
217- >>> mark_id = 1
218- >>> cur.execute("""
219- ... UPDATE TranslationMessage SET submitter=%(mark_id)s
220- ... WHERE id = %(new_posubmission_id)s
221- ... """, vars())
222- >>> mark_posubmission_id, date_touched = pofiletranslator(
223- ... mark_id, pofile_id)
224- >>> stub_posubmission_id, date_touched = pofiletranslator(
225- ... stub_id, pofile_id)
226- >>> mark_posubmission_id == stub_posubmission_id
227- False
228-
229-
230-The trigger was smart enough to locate the previous submission from stub
231-
232- >>> stub_posubmission_id == posubmission_id
233- True
234-
235
236=== modified file 'lib/lp/translations/tests/test_pofiletranslator.py'
237--- lib/lp/translations/tests/test_pofiletranslator.py 2011-12-28 17:03:06 +0000
238+++ lib/lp/translations/tests/test_pofiletranslator.py 2012-05-08 04:45:29 +0000
239@@ -1,14 +1,60 @@
240-# Copyright 2009 Canonical Ltd. This software is licensed under the
241+# Copyright 2009-2012 Canonical Ltd. This software is licensed under the
242 # GNU Affero General Public License version 3 (see the file LICENSE).
243
244 """Runs the POFileTranslator test."""
245
246 __metaclass__ = type
247
248-from lp.testing.layers import DatabaseLayer
249-from lp.testing.systemdocs import LayeredDocFileSuite
250-
251-
252-def test_suite():
253- return LayeredDocFileSuite(
254- 'pofiletranslator.txt', layer=DatabaseLayer)
255+from zope.component import getUtility
256+
257+from lp.testing import TestCaseWithFactory
258+from lp.testing.layers import ZopelessDatabaseLayer
259+from lp.translations.interfaces.pofiletranslator import IPOFileTranslatorSet
260+
261+
262+class TestPOFileTranslator(TestCaseWithFactory):
263+ layer = ZopelessDatabaseLayer
264+
265+ def test_getForPersonPOFile_returns_None_if_not_found(self):
266+ self.assertIsNone(
267+ getUtility(IPOFileTranslatorSet).getForPersonPOFile(
268+ self.factory.makePerson(), self.factory.makePOFile()))
269+
270+ def test_getForPersonPOFile_finds_record(self):
271+ pofile = self.factory.makePOFile()
272+ potmsgset = self.factory.makePOTMsgSet(pofile.potemplate)
273+ tm = self.factory.makeCurrentTranslationMessage(
274+ potmsgset=potmsgset, language=pofile.language)
275+ poft = getUtility(IPOFileTranslatorSet).getForPersonPOFile(
276+ tm.submitter, pofile)
277+ self.assertEqual(pofile, poft.pofile)
278+ self.assertEqual(tm.submitter, poft.person)
279+
280+ def test_getForPersonPOFile_ignores_other_persons(self):
281+ pofile = self.factory.makePOFile()
282+ potmsgset = self.factory.makePOTMsgSet(pofile.potemplate)
283+ self.factory.makeCurrentTranslationMessage(
284+ potmsgset=potmsgset, language=pofile.language)
285+ self.assertIsNone(
286+ getUtility(IPOFileTranslatorSet).getForPersonPOFile(
287+ self.factory.makePerson(), pofile))
288+
289+ def test_getForPersonPOFile_ignores_other_POFiles(self):
290+ pofile = self.factory.makePOFile('nl')
291+ potmsgset = self.factory.makePOTMsgSet(pofile.potemplate)
292+ tm = self.factory.makeCurrentTranslationMessage(
293+ potmsgset=potmsgset, language=pofile.language)
294+ other_pofile = self.factory.makePOFile('de', pofile.potemplate)
295+ self.assertIsNone(
296+ getUtility(IPOFileTranslatorSet).getForPersonPOFile(
297+ tm.submitter, other_pofile))
298+
299+ def test_getForTemplate_finds_all_for_template(self):
300+ pofile = self.factory.makePOFile()
301+ potmsgset = self.factory.makePOTMsgSet(pofile.potemplate)
302+ tm = self.factory.makeCurrentTranslationMessage(
303+ potmsgset=potmsgset, language=pofile.language)
304+ [poft] = list(
305+ getUtility(IPOFileTranslatorSet).getForTemplate(pofile.potemplate))
306+ self.assertEqual(pofile.potemplate, poft.pofile.potemplate)
307+ self.assertEqual(tm.submitter, poft.person)
308
309=== modified file 'lib/lp/translations/tests/test_translationmerger.py'
310--- lib/lp/translations/tests/test_translationmerger.py 2012-04-27 15:02:49 +0000
311+++ lib/lp/translations/tests/test_translationmerger.py 2012-05-08 04:45:29 +0000
312@@ -3,7 +3,6 @@
313
314 __metaclass__ = type
315
316-from datetime import timedelta
317 import gc
318 from logging import ERROR
319
320@@ -20,7 +19,6 @@
321 )
322 from lp.testing.layers import LaunchpadZopelessLayer
323 from lp.testing.sampledata import ADMIN_EMAIL
324-from lp.translations.interfaces.pofiletranslator import IPOFileTranslatorSet
325 from lp.translations.model.pomsgid import POMsgID
326 from lp.translations.model.potemplate import POTemplate
327 from lp.translations.model.potranslation import POTranslation
328@@ -510,59 +508,6 @@
329 tms = trunk_message.potmsgset.getAllTranslationMessages()
330 self.assertEqual(list(tms), [trunk_message])
331
332- # XXX: GavinPanella 2011-10-28 bug=883274: Spurious failure in buildbot.
333- def disabled_test_clashingPOFileTranslatorEntries(self):
334- # POFileTranslator is maintained by a trigger on
335- # TranslationMessage. Fiddling with TranslationTemplateItems
336- # directly bypasses it, so the script must make sure that
337- # POFileTranslator respects its unique constraints.
338-
339- # In this scenario, "trunk" has a TranslationMessage with a
340- # matching POFileTranslator entry. This message is happy where
341- # it is; it's not changing in any way during the test.
342- poftset = getUtility(IPOFileTranslatorSet)
343-
344- translator = self.trunk_template.owner
345- self.trunk_pofile.owner = translator
346- self.stable_pofile.owner = translator
347-
348- contented_potmsgset = self.factory.makePOTMsgSet(
349- self.trunk_template, singular='snut', sequence=2)
350- contented_message = self._makeTranslationMessage(
351- self.trunk_pofile, contented_potmsgset, 'druf', False)
352- self.assertEqual(translator, contented_message.submitter)
353- poft = poftset.getForPersonPOFile(translator, self.trunk_pofile)
354- self.assertEqual(poft.latest_message, contented_message)
355-
356- # Then there's the pair of POTMsgSets that are identical between
357- # trunk and stable. This one is translated only in stable.
358- # Merging will transfer that TranslationMessage from
359- # stable to trunk (where it becomes the shared message) through
360- # direct manipulation of TranslationTemplateItem.
361- stable_message = self._makeTranslationMessage(
362- self.stable_pofile, self.stable_potmsgset, 'fulb', False)
363- self.assertEqual(
364- stable_message.submitter, contented_message.submitter)
365-
366- stable_message = removeSecurityProxy(stable_message)
367-
368- # As it happens, this message is more recent than the happy one.
369- # This doesn't matter except it makes our test more predictable.
370- stable_message.date_created += timedelta(0, 0, 1)
371- poft = poftset.getForPersonPOFile(translator, self.stable_pofile)
372- self.assertEqual(poft.latest_message, stable_message)
373- removeSecurityProxy(poft).date_last_touched = (
374- stable_message.date_created)
375-
376- # Now the migration script runs. This also carries the
377- # POFileTranslator record for stable_message into trunk_pofile.
378- # The one for contented_message disappears in the process.
379- self.merger.mergePOTMsgSets()
380- self.merger.mergeTranslationMessages()
381-
382- poft = poftset.getForPersonPOFile(translator, self.trunk_pofile)
383- self.assertEqual(poft.latest_message, stable_message)
384-
385
386 class TestRemoveDuplicates(TestCaseWithFactory, TranslatedProductMixin):
387 """Test _scrubPOTMsgSetTranslations and friends."""
388
389=== modified file 'lib/lp/translations/translationmerger.py'
390--- lib/lp/translations/translationmerger.py 2012-01-01 02:58:52 +0000
391+++ lib/lp/translations/translationmerger.py 2012-05-08 04:45:29 +0000
392@@ -1,4 +1,4 @@
393-# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
394+# Copyright 2009-2012 Canonical Ltd. This software is licensed under the
395 # GNU Affero General Public License version 3 (see the file LICENSE).
396
397 __metaclass__ = type
398@@ -56,7 +56,7 @@
399 potmsgset.context)
400
401
402-def merge_pofiletranslators(from_potmsgset, to_template):
403+def merge_pofiletranslators(from_template, to_template):
404 """Merge POFileTranslator entries from one template into another.
405 """
406 # Import here to avoid circular import.
407@@ -64,7 +64,7 @@
408 IPOFileTranslatorSet)
409
410 pofiletranslatorset = getUtility(IPOFileTranslatorSet)
411- affected_rows = pofiletranslatorset.getForPOTMsgSet(from_potmsgset)
412+ affected_rows = pofiletranslatorset.getForTemplate(from_template)
413 for pofiletranslator in affected_rows:
414 person = pofiletranslator.person
415 from_pofile = pofiletranslator.pofile
416@@ -112,7 +112,7 @@
417 item.potmsgset = representative
418 templates.add(item.potemplate)
419
420- merge_pofiletranslators(item.potmsgset, representative_template)
421+ merge_pofiletranslators(item.potemplate, representative_template)
422
423
424 def filter_clashes(clashing_ubuntu, clashing_upstream, twin):