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
=== modified file 'lib/lp/translations/browser/tests/translationimportqueue-views.txt'
--- lib/lp/translations/browser/tests/translationimportqueue-views.txt 2010-10-18 22:24:59 +0000
+++ lib/lp/translations/browser/tests/translationimportqueue-views.txt 2011-01-12 09:00:40 +0000
@@ -89,7 +89,7 @@
89 ... 'name': 'demo-name',89 ... 'name': 'demo-name',
90 ... 'translation_domain': 'demo-domain'}90 ... 'translation_domain': 'demo-domain'}
91 >>> validate(view, data)91 >>> validate(view, data)
92 The file name must end with ".po".92 This filename is not appropriate for a translation.
93 Please choose a template.93 Please choose a template.
9494
95Also, the filename must be given and the template name must be a valid name.95Also, the filename must be given and the template name must be a valid name.
@@ -118,7 +118,7 @@
118 ... 'potemplate': potemplate,118 ... 'potemplate': potemplate,
119 ... 'language': language }119 ... 'language': language }
120 >>> validate(view, data)120 >>> validate(view, data)
121 The file name must end with ".pot".121 This filename is not appropriate for a template.
122 Please specify a name for the template.122 Please specify a name for the template.
123 Please specify a translation domain for the template.123 Please specify a translation domain for the template.
124124
125125
=== modified file 'lib/lp/translations/browser/translationimportqueue.py'
--- lib/lp/translations/browser/translationimportqueue.py 2010-12-02 16:13:51 +0000
+++ lib/lp/translations/browser/translationimportqueue.py 2011-01-12 09:00:40 +0000
@@ -14,10 +14,6 @@
14 ]14 ]
1515
16import os16import os
17from os.path import (
18 basename,
19 splitext,
20 )
2117
22from zope.app.form.interfaces import ConversionError18from zope.app.form.interfaces import ConversionError
23from zope.component import getUtility19from zope.component import getUtility
@@ -52,6 +48,9 @@
52from lp.translations.enums import RosettaImportStatus48from lp.translations.enums import RosettaImportStatus
53from lp.translations.interfaces.pofile import IPOFileSet49from lp.translations.interfaces.pofile import IPOFileSet
54from lp.translations.interfaces.potemplate import IPOTemplateSet50from lp.translations.interfaces.potemplate import IPOTemplateSet
51from lp.translations.interfaces.translationimporter import (
52 ITranslationImporter,
53 )
55from lp.translations.interfaces.translationimportqueue import (54from lp.translations.interfaces.translationimportqueue import (
56 IEditTranslationImportQueueEntry,55 IEditTranslationImportQueueEntry,
57 ITranslationImportQueue,56 ITranslationImportQueue,
@@ -102,11 +101,12 @@
102 return field_values101 return field_values
103 # Fill the know values.102 # Fill the know values.
104 field_values['path'] = self.context.path103 field_values['path'] = self.context.path
105 (fname, fext) = splitext(self.context.path)104
106 if fext.lower() == '.po':105 importer = getUtility(ITranslationImporter)
106 if importer.isTemplateName(self.context.path):
107 file_type = TranslationFileType.POT
108 elif importer.isTranslationName(self.context.path):
107 file_type = TranslationFileType.PO109 file_type = TranslationFileType.PO
108 elif fext.lower() == '.pot':
109 file_type = TranslationFileType.POT
110 else:110 else:
111 file_type = TranslationFileType.UNSPEC111 file_type = TranslationFileType.UNSPEC
112 field_values['file_type'] = file_type112 field_values['file_type'] = file_type
@@ -323,56 +323,59 @@
323 productseries=self.context.productseries)323 productseries=self.context.productseries)
324 return potemplate_subset324 return potemplate_subset
325325
326 def _findObjectionToFilePath(self, file_type, path):
327 """Return textual objection, if any, to setting this file path."""
328 importer = getUtility(ITranslationImporter)
329 if file_type == TranslationFileType.POT:
330 if not importer.isTemplateName(path):
331 return "This filename is not appropriate for a template."
332 else:
333 if not importer.isTranslationName(path):
334 return "This filename is not appropriate for a translation."
335
336 if path == self.context.path:
337 # No change, so no objections.
338 return None
339
340 # The Rosetta Expert decided to change the path of the file.
341 # Before accepting such change, we should check first whether
342 # there is already another entry with that path in the same
343 # context (sourcepackagename/distroseries or productseries).
344 # A duplicate name will confuse the auto-approval
345 # process.
346 if file_type == TranslationFileType.POT:
347 potemplate_set = getUtility(IPOTemplateSet)
348 existing_file = potemplate_set.getPOTemplateByPathAndOrigin(
349 path, self.context.productseries, self.context.distroseries,
350 self.context.sourcepackagename)
351 already_exists = existing_file is not None
352 else:
353 pofile_set = getUtility(IPOFileSet)
354 existing_files = pofile_set.getPOFilesByPathAndOrigin(
355 path, self.context.productseries,
356 self.context.distroseries,
357 self.context.sourcepackagename)
358 already_exists = not existing_files.is_empty()
359
360 if already_exists:
361 # We already have an IPOFile in this path, let's notify
362 # the user about that so they choose another path.
363 return "There is already a file in the given path."
364
365 return None
366
326 def _validatePath(self, file_type, path):367 def _validatePath(self, file_type, path):
327 path_changed = False # Flag for change_action368 """Should the entry's path be updated?"""
328 if path == None or path.strip() == "":369 if path is None or path.strip() == "":
329 self.setFieldError('path', 'The file name is missing.')370 self.setFieldError('path', "The file name is missing.")
371 return False
372
373 objection = self._findObjectionToFilePath(file_type, path)
374 if objection is None:
375 return True
330 else:376 else:
331 (fname, fext) = splitext(basename(path))377 self.setFieldError('path', objection)
332 if len(fname) == 0:378 return False
333 self.setFieldError('path',
334 'The file name is incomplete.')
335 if (file_type == TranslationFileType.POT and
336 fext.lower() != '.pot' and fext.lower() != '.xpi'):
337 self.setFieldError('path',
338 'The file name must end with ".pot".')
339 if (file_type == TranslationFileType.PO and
340 fext.lower() != '.po' and fext.lower() != '.xpi'):
341 self.setFieldError('path',
342 'The file name must end with ".po".')
343
344 if self.context.path != path:
345 # The Rosetta Expert decided to change the path of the file.
346 # Before accepting such change, we should check first whether
347 # there is already another entry with that path in the same
348 # context (sourcepackagename/distroseries or productseries).
349 if file_type == TranslationFileType.POT:
350 potemplate_set = getUtility(IPOTemplateSet)
351 existing_file = (
352 potemplate_set.getPOTemplateByPathAndOrigin(
353 path, self.context.productseries,
354 self.context.distroseries,
355 self.context.sourcepackagename))
356 already_exists = existing_file is not None
357 else:
358 pofile_set = getUtility(IPOFileSet)
359 existing_files = pofile_set.getPOFilesByPathAndOrigin(
360 path, self.context.productseries,
361 self.context.distroseries,
362 self.context.sourcepackagename)
363 already_exists = not existing_files.is_empty()
364
365 if already_exists:
366 # We already have an IPOFile in this path, let's notify
367 # the user about that so they choose another path.
368 self.setFieldError('path',
369 'There is already a file in the given path.')
370 else:
371 # There is no other pofile in the given path for this
372 # context, let's change it as requested by admins.
373 path_changed = True
374
375 return path_changed
376379
377 def _validatePOT(self, data):380 def _validatePOT(self, data):
378 name = data.get('name')381 name = data.get('name')
379382
=== modified file 'lib/lp/translations/model/pofile.py'
--- lib/lp/translations/model/pofile.py 2010-12-30 17:22:40 +0000
+++ lib/lp/translations/model/pofile.py 2011-01-12 09:00:40 +0000
@@ -1473,7 +1473,8 @@
1473 return DummyPOFile(potemplate, language)1473 return DummyPOFile(potemplate, language)
14741474
1475 def getPOFilesByPathAndOrigin(self, path, productseries=None,1475 def getPOFilesByPathAndOrigin(self, path, productseries=None,
1476 distroseries=None, sourcepackagename=None):1476 distroseries=None, sourcepackagename=None,
1477 ignore_obsolete=False):
1477 """See `IPOFileSet`."""1478 """See `IPOFileSet`."""
1478 # Avoid circular imports.1479 # Avoid circular imports.
1479 from lp.translations.model.potemplate import POTemplate1480 from lp.translations.model.potemplate import POTemplate
@@ -1492,35 +1493,35 @@
14921493
1493 store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)1494 store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
14941495
1495 general_conditions = And(1496 conditions = [
1496 POFile.path == path,1497 POFile.path == path,
1497 POFile.potemplate == POTemplate.id)1498 POFile.potemplate == POTemplate.id,
1499 ]
1500 if ignore_obsolete:
1501 conditions.append(POTemplate.iscurrent == True)
14981502
1499 if productseries is not None:1503 if productseries is not None:
1500 return store.find(POFile, And(1504 conditions.append(POTemplate.productseries == productseries)
1501 general_conditions,
1502 POTemplate.productseries == productseries))
1503 else:1505 else:
1504 distro_conditions = And(1506 conditions.append(POTemplate.distroseries == distroseries)
1505 general_conditions,
1506 POTemplate.distroseries == distroseries)
15071507
1508 # The POFile belongs to a distribution and it could come from1508 # The POFile belongs to a distribution and it could come from
1509 # another package that its POTemplate is linked to, so we first1509 # another package that its POTemplate is linked to, so we first
1510 # check to find it at IPOFile.from_sourcepackagename1510 # check to find it at IPOFile.from_sourcepackagename
1511 matches = store.find(POFile, And(1511 linked_conditions = conditions + [
1512 distro_conditions,1512 POFile.from_sourcepackagename == sourcepackagename]
1513 POFile.from_sourcepackagename == sourcepackagename))
15141513
1514 matches = store.find(POFile, *linked_conditions)
1515 if not matches.is_empty():1515 if not matches.is_empty():
1516 return matches1516 return matches
15171517
1518 # There is no pofile in that 'path' and1518 # There is no pofile in that 'path' and
1519 # 'IPOFile.from_sourcepackagename' so we do a search using the1519 # 'IPOFile.from_sourcepackagename' so we do a search using the
1520 # usual sourcepackagename.1520 # usual sourcepackagename.
1521 return store.find(POFile, And(1521 conditions.append(
1522 distro_conditions,1522 POTemplate.sourcepackagename == sourcepackagename)
1523 POTemplate.sourcepackagename == sourcepackagename))1523
1524 return store.find(POFile, *conditions)
15241525
1525 def getBatch(self, starting_id, batch_size):1526 def getBatch(self, starting_id, batch_size):
1526 """See `IPOFileSet`."""1527 """See `IPOFileSet`."""
15271528
=== modified file 'lib/lp/translations/model/translationimportqueue.py'
--- lib/lp/translations/model/translationimportqueue.py 2010-12-18 19:40:37 +0000
+++ lib/lp/translations/model/translationimportqueue.py 2011-01-12 09:00:40 +0000
@@ -293,7 +293,8 @@
293 return pofile_set.getPOFilesByPathAndOrigin(293 return pofile_set.getPOFilesByPathAndOrigin(
294 self.path, productseries=self.productseries,294 self.path, productseries=self.productseries,
295 distroseries=self.distroseries,295 distroseries=self.distroseries,
296 sourcepackagename=self.sourcepackagename).one()296 sourcepackagename=self.sourcepackagename,
297 ignore_obsolete=True).one()
297298
298 def canAdmin(self, roles):299 def canAdmin(self, roles):
299 """See `ITranslationImportQueueEntry`."""300 """See `ITranslationImportQueueEntry`."""
300301
=== modified file 'lib/lp/translations/tests/test_autoapproval.py'
--- lib/lp/translations/tests/test_autoapproval.py 2010-10-29 05:58:51 +0000
+++ lib/lp/translations/tests/test_autoapproval.py 2011-01-12 09:00:40 +0000
@@ -37,6 +37,7 @@
37from lp.translations.enums import RosettaImportStatus37from lp.translations.enums import RosettaImportStatus
38from lp.translations.interfaces.customlanguagecode import ICustomLanguageCode38from lp.translations.interfaces.customlanguagecode import ICustomLanguageCode
39from lp.translations.interfaces.translationimportqueue import (39from lp.translations.interfaces.translationimportqueue import (
40 ITranslationImportQueue,
40 translation_import_queue_entry_age,41 translation_import_queue_entry_age,
41 )42 )
42from lp.translations.model.customlanguagecode import CustomLanguageCode43from lp.translations.model.customlanguagecode import CustomLanguageCode
@@ -536,6 +537,36 @@
536537
537 self.assertEqual(entry1.potemplate, None)538 self.assertEqual(entry1.potemplate, None)
538539
540 def test_getGuessedPOFile_ignores_obsolete_POFiles(self):
541 pofile = self.factory.makePOFile()
542 template = pofile.potemplate
543 template.iscurrent = False
544 queue = getUtility(ITranslationImportQueue)
545 entry = queue.addOrUpdateEntry(
546 pofile.path, 'contents', False, self.factory.makePerson(),
547 productseries=template.productseries)
548
549 self.assertEqual(None, entry.getGuessedPOFile())
550
551 def test_getGuessedPOFile_survives_clashing_obsolete_POFile_path(self):
552 series = self.factory.makeProductSeries()
553 current_template = self.factory.makePOTemplate(productseries=series)
554 current_template.iscurrent = True
555 current_pofile = self.factory.makePOFile(
556 'nl', potemplate=current_template)
557 obsolete_template = self.factory.makePOTemplate(productseries=series)
558 obsolete_template.iscurrent = False
559 obsolete_pofile = self.factory.makePOFile(
560 'nl', potemplate=obsolete_template)
561 obsolete_pofile.path = current_pofile.path
562
563 queue = getUtility(ITranslationImportQueue)
564 entry = queue.addOrUpdateEntry(
565 current_pofile.path, 'contents', False, self.factory.makePerson(),
566 productseries=series)
567
568 self.assertEqual(current_pofile, entry.getGuessedPOFile())
569
539 def test_pathless_template_match(self):570 def test_pathless_template_match(self):
540 # If an uploaded template has no directory component in its571 # If an uploaded template has no directory component in its
541 # path, and no matching template is found in the database, the572 # path, and no matching template is found in the database, the
542573
=== modified file 'lib/lp/translations/tests/test_pofile.py'
--- lib/lp/translations/tests/test_pofile.py 2011-01-05 14:23:48 +0000
+++ lib/lp/translations/tests/test_pofile.py 2011-01-12 09:00:40 +0000
@@ -1714,6 +1714,25 @@
17141714
1715 self.assertContentEqual([pofile], found)1715 self.assertContentEqual([pofile], found)
17161716
1717 def test_getPOFilesByPathAndOrigin_includes_obsolete_templates(self):
1718 pofile = self.factory.makePOFile()
1719 template = pofile.potemplate
1720 template.iscurrent = False
1721 self.assertContentEqual(
1722 [pofile],
1723 self.pofileset.getPOFilesByPathAndOrigin(
1724 pofile.path, productseries=template.productseries))
1725
1726 def test_getPOFilesByPathAndOrigin_can_ignore_obsolete_templates(self):
1727 pofile = self.factory.makePOFile()
1728 template = pofile.potemplate
1729 template.iscurrent = False
1730 self.assertContentEqual(
1731 [],
1732 self.pofileset.getPOFilesByPathAndOrigin(
1733 pofile.path, productseries=template.productseries,
1734 ignore_obsolete=True))
1735
17171736
1718class TestPOFileStatistics(TestCaseWithFactory):1737class TestPOFileStatistics(TestCaseWithFactory):
1719 """Test PO files statistics calculation."""1738 """Test PO files statistics calculation."""