Merge lp:~jtv/launchpad/bug-611217 into lp:launchpad

Proposed by Jeroen T. Vermeulen
Status: Merged
Approved by: Māris Fogels
Approved revision: no longer in the source branch.
Merged at revision: 12216
Proposed branch: lp:~jtv/launchpad/bug-611217
Merge into: lp:launchpad
Diff against target: 328 lines (+129/-74)
6 files modified
lib/lp/translations/browser/tests/translationimportqueue-views.txt (+2/-2)
lib/lp/translations/browser/translationimportqueue.py (+59/-56)
lib/lp/translations/model/pofile.py (+16/-15)
lib/lp/translations/model/translationimportqueue.py (+2/-1)
lib/lp/translations/tests/test_autoapproval.py (+31/-0)
lib/lp/translations/tests/test_pofile.py (+19/-0)
To merge this branch: bzr merge lp:~jtv/launchpad/bug-611217
Reviewer Review Type Date Requested Status
Māris Fogels (community) Approve
Launchpad code reviewers code Pending
Review via email: mp+45883@code.launchpad.net

Commit message

[r=mars][ui=none][bug=611217] Ignore POFiles for obsolete POTemplates during upload approval.

Description of the change

= Bug 611217 =

Not for the first time, the translations import queue gardener broke down recently because of name clashes.

The problem is this: a queue entry contains a translation file for a given productseries or source package (in this case it happened to be git in Ubuntu 10.10). The gardener's auto-approval code tries to match this to an existing POFile, based on a path match in that productseries or source package. POFileSet.getPOFilesByPathAndOrigin implements the match.

That combination of path and productseries/package should normally be unique, and so TranslationImportQueueEntry._guessed_pofile_from_path returns getPOFilesByPathAndOrigin(…).one(). But it is occasionally possible for a POFile belonging to an obsolete POTemplate to have the same path as another POFile belonging a current POTemplate in the same context. KABOOM. The one() unexpectedly finds two records, and blows up.

Actually we have no particular interest in auto-approving translations for obsolete templates anyway. So in this branch I make _guessed_pofile_from_path ignore POFiles for obsolete templates. That avoids the clashes as a side effect.

In addition to this, I cleaned up some browser code that unnecessarily duplicated the logic of figuring out filename extensions (which is also done by the importer). I also found that it clashed with our coding standards and could generally be simpler, so that turned into a bit of a local rewrite. The only practical effect is that the notions of "template file" and "translation file" are no longer directly coupled to gettext ".po" and ".pot" files, respectively.

I ran various tests, but this should just about cover it:
{{{
./bin/test -vvc lp.translations -t queue -t approv -t test_pofile
}}}

The sign that this works will be that we no longer get these crises where Ubuntu and manual translation uploads stop being approved. For active Q/A, we'll want to make sure that manual uploads are still approved automatically in each of 3 scenarios:
 * There is a POFile with a matching path.
 * There is a POFile for the language, but with a different path.
 * There is no POFile for the language.

No lint,

Jeroen

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

Hi Jeroen,

This change looks great, r=mars.

I think you have one stray if-statement around line 72 of the diff: you can probably collapse that into a straight if/elif instead of nested statements.

Looks good!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/translations/browser/tests/translationimportqueue-views.txt'
2--- lib/lp/translations/browser/tests/translationimportqueue-views.txt 2010-10-18 22:24:59 +0000
3+++ lib/lp/translations/browser/tests/translationimportqueue-views.txt 2011-01-12 09:00:40 +0000
4@@ -89,7 +89,7 @@
5 ... 'name': 'demo-name',
6 ... 'translation_domain': 'demo-domain'}
7 >>> validate(view, data)
8- The file name must end with ".po".
9+ This filename is not appropriate for a translation.
10 Please choose a template.
11
12 Also, the filename must be given and the template name must be a valid name.
13@@ -118,7 +118,7 @@
14 ... 'potemplate': potemplate,
15 ... 'language': language }
16 >>> validate(view, data)
17- The file name must end with ".pot".
18+ This filename is not appropriate for a template.
19 Please specify a name for the template.
20 Please specify a translation domain for the template.
21
22
23=== modified file 'lib/lp/translations/browser/translationimportqueue.py'
24--- lib/lp/translations/browser/translationimportqueue.py 2010-12-02 16:13:51 +0000
25+++ lib/lp/translations/browser/translationimportqueue.py 2011-01-12 09:00:40 +0000
26@@ -14,10 +14,6 @@
27 ]
28
29 import os
30-from os.path import (
31- basename,
32- splitext,
33- )
34
35 from zope.app.form.interfaces import ConversionError
36 from zope.component import getUtility
37@@ -52,6 +48,9 @@
38 from lp.translations.enums import RosettaImportStatus
39 from lp.translations.interfaces.pofile import IPOFileSet
40 from lp.translations.interfaces.potemplate import IPOTemplateSet
41+from lp.translations.interfaces.translationimporter import (
42+ ITranslationImporter,
43+ )
44 from lp.translations.interfaces.translationimportqueue import (
45 IEditTranslationImportQueueEntry,
46 ITranslationImportQueue,
47@@ -102,11 +101,12 @@
48 return field_values
49 # Fill the know values.
50 field_values['path'] = self.context.path
51- (fname, fext) = splitext(self.context.path)
52- if fext.lower() == '.po':
53+
54+ importer = getUtility(ITranslationImporter)
55+ if importer.isTemplateName(self.context.path):
56+ file_type = TranslationFileType.POT
57+ elif importer.isTranslationName(self.context.path):
58 file_type = TranslationFileType.PO
59- elif fext.lower() == '.pot':
60- file_type = TranslationFileType.POT
61 else:
62 file_type = TranslationFileType.UNSPEC
63 field_values['file_type'] = file_type
64@@ -323,56 +323,59 @@
65 productseries=self.context.productseries)
66 return potemplate_subset
67
68+ def _findObjectionToFilePath(self, file_type, path):
69+ """Return textual objection, if any, to setting this file path."""
70+ importer = getUtility(ITranslationImporter)
71+ if file_type == TranslationFileType.POT:
72+ if not importer.isTemplateName(path):
73+ return "This filename is not appropriate for a template."
74+ else:
75+ if not importer.isTranslationName(path):
76+ return "This filename is not appropriate for a translation."
77+
78+ if path == self.context.path:
79+ # No change, so no objections.
80+ return None
81+
82+ # The Rosetta Expert decided to change the path of the file.
83+ # Before accepting such change, we should check first whether
84+ # there is already another entry with that path in the same
85+ # context (sourcepackagename/distroseries or productseries).
86+ # A duplicate name will confuse the auto-approval
87+ # process.
88+ if file_type == TranslationFileType.POT:
89+ potemplate_set = getUtility(IPOTemplateSet)
90+ existing_file = potemplate_set.getPOTemplateByPathAndOrigin(
91+ path, self.context.productseries, self.context.distroseries,
92+ self.context.sourcepackagename)
93+ already_exists = existing_file is not None
94+ else:
95+ pofile_set = getUtility(IPOFileSet)
96+ existing_files = pofile_set.getPOFilesByPathAndOrigin(
97+ path, self.context.productseries,
98+ self.context.distroseries,
99+ self.context.sourcepackagename)
100+ already_exists = not existing_files.is_empty()
101+
102+ if already_exists:
103+ # We already have an IPOFile in this path, let's notify
104+ # the user about that so they choose another path.
105+ return "There is already a file in the given path."
106+
107+ return None
108+
109 def _validatePath(self, file_type, path):
110- path_changed = False # Flag for change_action
111- if path == None or path.strip() == "":
112- self.setFieldError('path', 'The file name is missing.')
113+ """Should the entry's path be updated?"""
114+ if path is None or path.strip() == "":
115+ self.setFieldError('path', "The file name is missing.")
116+ return False
117+
118+ objection = self._findObjectionToFilePath(file_type, path)
119+ if objection is None:
120+ return True
121 else:
122- (fname, fext) = splitext(basename(path))
123- if len(fname) == 0:
124- self.setFieldError('path',
125- 'The file name is incomplete.')
126- if (file_type == TranslationFileType.POT and
127- fext.lower() != '.pot' and fext.lower() != '.xpi'):
128- self.setFieldError('path',
129- 'The file name must end with ".pot".')
130- if (file_type == TranslationFileType.PO and
131- fext.lower() != '.po' and fext.lower() != '.xpi'):
132- self.setFieldError('path',
133- 'The file name must end with ".po".')
134-
135- if self.context.path != path:
136- # The Rosetta Expert decided to change the path of the file.
137- # Before accepting such change, we should check first whether
138- # there is already another entry with that path in the same
139- # context (sourcepackagename/distroseries or productseries).
140- if file_type == TranslationFileType.POT:
141- potemplate_set = getUtility(IPOTemplateSet)
142- existing_file = (
143- potemplate_set.getPOTemplateByPathAndOrigin(
144- path, self.context.productseries,
145- self.context.distroseries,
146- self.context.sourcepackagename))
147- already_exists = existing_file is not None
148- else:
149- pofile_set = getUtility(IPOFileSet)
150- existing_files = pofile_set.getPOFilesByPathAndOrigin(
151- path, self.context.productseries,
152- self.context.distroseries,
153- self.context.sourcepackagename)
154- already_exists = not existing_files.is_empty()
155-
156- if already_exists:
157- # We already have an IPOFile in this path, let's notify
158- # the user about that so they choose another path.
159- self.setFieldError('path',
160- 'There is already a file in the given path.')
161- else:
162- # There is no other pofile in the given path for this
163- # context, let's change it as requested by admins.
164- path_changed = True
165-
166- return path_changed
167+ self.setFieldError('path', objection)
168+ return False
169
170 def _validatePOT(self, data):
171 name = data.get('name')
172
173=== modified file 'lib/lp/translations/model/pofile.py'
174--- lib/lp/translations/model/pofile.py 2010-12-30 17:22:40 +0000
175+++ lib/lp/translations/model/pofile.py 2011-01-12 09:00:40 +0000
176@@ -1473,7 +1473,8 @@
177 return DummyPOFile(potemplate, language)
178
179 def getPOFilesByPathAndOrigin(self, path, productseries=None,
180- distroseries=None, sourcepackagename=None):
181+ distroseries=None, sourcepackagename=None,
182+ ignore_obsolete=False):
183 """See `IPOFileSet`."""
184 # Avoid circular imports.
185 from lp.translations.model.potemplate import POTemplate
186@@ -1492,35 +1493,35 @@
187
188 store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
189
190- general_conditions = And(
191+ conditions = [
192 POFile.path == path,
193- POFile.potemplate == POTemplate.id)
194+ POFile.potemplate == POTemplate.id,
195+ ]
196+ if ignore_obsolete:
197+ conditions.append(POTemplate.iscurrent == True)
198
199 if productseries is not None:
200- return store.find(POFile, And(
201- general_conditions,
202- POTemplate.productseries == productseries))
203+ conditions.append(POTemplate.productseries == productseries)
204 else:
205- distro_conditions = And(
206- general_conditions,
207- POTemplate.distroseries == distroseries)
208+ conditions.append(POTemplate.distroseries == distroseries)
209
210 # The POFile belongs to a distribution and it could come from
211 # another package that its POTemplate is linked to, so we first
212 # check to find it at IPOFile.from_sourcepackagename
213- matches = store.find(POFile, And(
214- distro_conditions,
215- POFile.from_sourcepackagename == sourcepackagename))
216+ linked_conditions = conditions + [
217+ POFile.from_sourcepackagename == sourcepackagename]
218
219+ matches = store.find(POFile, *linked_conditions)
220 if not matches.is_empty():
221 return matches
222
223 # There is no pofile in that 'path' and
224 # 'IPOFile.from_sourcepackagename' so we do a search using the
225 # usual sourcepackagename.
226- return store.find(POFile, And(
227- distro_conditions,
228- POTemplate.sourcepackagename == sourcepackagename))
229+ conditions.append(
230+ POTemplate.sourcepackagename == sourcepackagename)
231+
232+ return store.find(POFile, *conditions)
233
234 def getBatch(self, starting_id, batch_size):
235 """See `IPOFileSet`."""
236
237=== modified file 'lib/lp/translations/model/translationimportqueue.py'
238--- lib/lp/translations/model/translationimportqueue.py 2010-12-18 19:40:37 +0000
239+++ lib/lp/translations/model/translationimportqueue.py 2011-01-12 09:00:40 +0000
240@@ -293,7 +293,8 @@
241 return pofile_set.getPOFilesByPathAndOrigin(
242 self.path, productseries=self.productseries,
243 distroseries=self.distroseries,
244- sourcepackagename=self.sourcepackagename).one()
245+ sourcepackagename=self.sourcepackagename,
246+ ignore_obsolete=True).one()
247
248 def canAdmin(self, roles):
249 """See `ITranslationImportQueueEntry`."""
250
251=== modified file 'lib/lp/translations/tests/test_autoapproval.py'
252--- lib/lp/translations/tests/test_autoapproval.py 2010-10-29 05:58:51 +0000
253+++ lib/lp/translations/tests/test_autoapproval.py 2011-01-12 09:00:40 +0000
254@@ -37,6 +37,7 @@
255 from lp.translations.enums import RosettaImportStatus
256 from lp.translations.interfaces.customlanguagecode import ICustomLanguageCode
257 from lp.translations.interfaces.translationimportqueue import (
258+ ITranslationImportQueue,
259 translation_import_queue_entry_age,
260 )
261 from lp.translations.model.customlanguagecode import CustomLanguageCode
262@@ -536,6 +537,36 @@
263
264 self.assertEqual(entry1.potemplate, None)
265
266+ def test_getGuessedPOFile_ignores_obsolete_POFiles(self):
267+ pofile = self.factory.makePOFile()
268+ template = pofile.potemplate
269+ template.iscurrent = False
270+ queue = getUtility(ITranslationImportQueue)
271+ entry = queue.addOrUpdateEntry(
272+ pofile.path, 'contents', False, self.factory.makePerson(),
273+ productseries=template.productseries)
274+
275+ self.assertEqual(None, entry.getGuessedPOFile())
276+
277+ def test_getGuessedPOFile_survives_clashing_obsolete_POFile_path(self):
278+ series = self.factory.makeProductSeries()
279+ current_template = self.factory.makePOTemplate(productseries=series)
280+ current_template.iscurrent = True
281+ current_pofile = self.factory.makePOFile(
282+ 'nl', potemplate=current_template)
283+ obsolete_template = self.factory.makePOTemplate(productseries=series)
284+ obsolete_template.iscurrent = False
285+ obsolete_pofile = self.factory.makePOFile(
286+ 'nl', potemplate=obsolete_template)
287+ obsolete_pofile.path = current_pofile.path
288+
289+ queue = getUtility(ITranslationImportQueue)
290+ entry = queue.addOrUpdateEntry(
291+ current_pofile.path, 'contents', False, self.factory.makePerson(),
292+ productseries=series)
293+
294+ self.assertEqual(current_pofile, entry.getGuessedPOFile())
295+
296 def test_pathless_template_match(self):
297 # If an uploaded template has no directory component in its
298 # path, and no matching template is found in the database, the
299
300=== modified file 'lib/lp/translations/tests/test_pofile.py'
301--- lib/lp/translations/tests/test_pofile.py 2011-01-05 14:23:48 +0000
302+++ lib/lp/translations/tests/test_pofile.py 2011-01-12 09:00:40 +0000
303@@ -1714,6 +1714,25 @@
304
305 self.assertContentEqual([pofile], found)
306
307+ def test_getPOFilesByPathAndOrigin_includes_obsolete_templates(self):
308+ pofile = self.factory.makePOFile()
309+ template = pofile.potemplate
310+ template.iscurrent = False
311+ self.assertContentEqual(
312+ [pofile],
313+ self.pofileset.getPOFilesByPathAndOrigin(
314+ pofile.path, productseries=template.productseries))
315+
316+ def test_getPOFilesByPathAndOrigin_can_ignore_obsolete_templates(self):
317+ pofile = self.factory.makePOFile()
318+ template = pofile.potemplate
319+ template.iscurrent = False
320+ self.assertContentEqual(
321+ [],
322+ self.pofileset.getPOFilesByPathAndOrigin(
323+ pofile.path, productseries=template.productseries,
324+ ignore_obsolete=True))
325+
326
327 class TestPOFileStatistics(TestCaseWithFactory):
328 """Test PO files statistics calculation."""