Merge lp:~danilo/launchpad/bug-302449 into lp:launchpad

Proposed by Данило Шеган
Status: Merged
Approved by: Brad Crittenden
Approved revision: no longer in the source branch.
Merged at revision: 13922
Proposed branch: lp:~danilo/launchpad/bug-302449
Merge into: lp:launchpad
Diff against target: 277 lines (+78/-56)
7 files modified
lib/lp/testing/factory.py (+14/-11)
lib/lp/translations/browser/potemplate.py (+4/-1)
lib/lp/translations/doc/sourcepackagerelease-translations.txt (+2/-2)
lib/lp/translations/model/translationimportqueue.py (+32/-34)
lib/lp/translations/tests/test_autoapproval.py (+7/-5)
lib/lp/translations/tests/test_translationimportqueue.py (+18/-0)
lib/lp/translations/utilities/tests/test_translation_importer.py (+1/-3)
To merge this branch: bzr merge lp:~danilo/launchpad/bug-302449
Reviewer Review Type Date Requested Status
Brad Crittenden (community) code Approve
Review via email: mp+73813@code.launchpad.net

Commit message

[r=bac][bug=302449] Make sure TranslationImportQueue.addOrUpdateEntry doesn't hit a DB constraint on (importer, potemplate, path, target).

Description of the change

= Bug 302449 =

TranslationImportQueue._getMatchingEntry logic does not match the
actual DB constraint, which leads to OOPSes (it is less restrictive than the constraint).

The problem is that it requires a 'pofile' on the TranslationImportQueueEntry table row if pofile is specified, yet the constraint is on the template/path/importer/target.

== Proposed fix ==

Switch it to storm syntax and make pofile clause an Or() with pofile==None.

Drive-by fix for link visibility for POTemplate:+admin page.

It's still possible to hit the same constraint modifying the import queue entry directly, but that's a different problem.

== Tests ==

bin/test -cvvt addOrUpdateEntry -t translationimportqueue.txt

(unfortunately, this dates back to when we did a lot of unit testing in doctests)

== Demo and Q/A ==

Upload a tarball with a translation file on the POTemplate:+upload page, and then upload identical PO file with the same path on the POFile:+upload page. It should not OOPS anymore.

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/translations/model/translationimportqueue.py
  lib/lp/testing/factory.py
  lib/lp/translations/tests/test_translationimportqueue.py
  lib/lp/translations/browser/potemplate.py

To post a comment you must log in.
Revision history for this message
Brad Crittenden (bac) wrote :

Looks good Danilo, except for the lint issues you say you've already fixed.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py 2011-09-12 07:29:31 +0000
+++ lib/lp/testing/factory.py 2011-09-12 15:16:27 +0000
@@ -306,6 +306,9 @@
306 TranslationFileFormat,306 TranslationFileFormat,
307 )307 )
308from lp.translations.interfaces.translationgroup import ITranslationGroupSet308from lp.translations.interfaces.translationgroup import ITranslationGroupSet
309from lp.translations.interfaces.translationimportqueue import (
310 ITranslationImportQueue,
311 )
309from lp.translations.interfaces.translationmessage import (312from lp.translations.interfaces.translationmessage import (
310 RosettaTranslationOrigin,313 RosettaTranslationOrigin,
311 )314 )
@@ -314,9 +317,6 @@
314 ITranslationTemplatesBuildJobSource,317 ITranslationTemplatesBuildJobSource,
315 )318 )
316from lp.translations.interfaces.translator import ITranslatorSet319from lp.translations.interfaces.translator import ITranslatorSet
317from lp.translations.model.translationimportqueue import (
318 TranslationImportQueueEntry,
319 )
320from lp.translations.model.translationtemplateitem import (320from lp.translations.model.translationtemplateitem import (
321 TranslationTemplateItem,321 TranslationTemplateItem,
322 )322 )
@@ -3241,9 +3241,6 @@
32413241
3242 if content is None:3242 if content is None:
3243 content = self.getUniqueString()3243 content = self.getUniqueString()
3244 content_reference = getUtility(ILibraryFileAliasSet).create(
3245 name=os.path.basename(path), size=len(content),
3246 file=StringIO(content), contentType='text/plain')
32473244
3248 if format is None:3245 if format is None:
3249 format = TranslationFileFormat.PO3246 format = TranslationFileFormat.PO
@@ -3251,11 +3248,17 @@
3251 if status is None:3248 if status is None:
3252 status = RosettaImportStatus.NEEDS_REVIEW3249 status = RosettaImportStatus.NEEDS_REVIEW
32533250
3254 return TranslationImportQueueEntry(3251 if type(content) == unicode:
3255 path=path, productseries=productseries, distroseries=distroseries,3252 content = content.encode('utf-8')
3256 sourcepackagename=sourcepackagename, importer=uploader,3253
3257 content=content_reference, status=status, format=format,3254 entry = getUtility(ITranslationImportQueue).addOrUpdateEntry(
3258 by_maintainer=by_maintainer)3255 path=path, content=content, by_maintainer=by_maintainer,
3256 importer=uploader, productseries=productseries,
3257 distroseries=distroseries, sourcepackagename=sourcepackagename,
3258 potemplate=potemplate, pofile=pofile, format=format)
3259 entry.setStatus(
3260 status, getUtility(ILaunchpadCelebrities).rosetta_experts)
3261 return entry
32593262
3260 def makeMailingList(self, team, owner):3263 def makeMailingList(self, team, owner):
3261 """Create a mailing list for the team."""3264 """Create a mailing list for the team."""
32623265
=== modified file 'lib/lp/translations/browser/potemplate.py'
--- lib/lp/translations/browser/potemplate.py 2011-08-25 19:48:19 +0000
+++ lib/lp/translations/browser/potemplate.py 2011-09-12 15:16:27 +0000
@@ -76,6 +76,7 @@
76from lp.registry.browser.productseries import ProductSeriesFacets76from lp.registry.browser.productseries import ProductSeriesFacets
77from lp.registry.browser.sourcepackage import SourcePackageFacets77from lp.registry.browser.sourcepackage import SourcePackageFacets
78from lp.registry.interfaces.productseries import IProductSeries78from lp.registry.interfaces.productseries import IProductSeries
79from lp.registry.interfaces.role import IPersonRoles
79from lp.registry.interfaces.sourcepackage import ISourcePackage80from lp.registry.interfaces.sourcepackage import ISourcePackage
80from lp.registry.model.packaging import Packaging81from lp.registry.model.packaging import Packaging
81from lp.registry.model.product import Product82from lp.registry.model.product import Product
@@ -928,7 +929,9 @@
928 self.distroseries = series929 self.distroseries = series
929 else:930 else:
930 self.productseries = series931 self.productseries = series
931 self.can_admin = check_permission('launchpad.Admin', series)932 user = IPersonRoles(self.user, None)
933 self.can_admin = (user is not None and
934 (user.in_admin or user.in_rosetta_experts))
932 self.can_edit = (935 self.can_edit = (
933 self.can_admin or936 self.can_admin or
934 check_permission('launchpad.TranslationsAdmin', series))937 check_permission('launchpad.TranslationsAdmin', series))
935938
=== modified file 'lib/lp/translations/doc/sourcepackagerelease-translations.txt'
--- lib/lp/translations/doc/sourcepackagerelease-translations.txt 2011-05-27 19:53:20 +0000
+++ lib/lp/translations/doc/sourcepackagerelease-translations.txt 2011-09-12 15:16:27 +0000
@@ -65,7 +65,7 @@
6565
66 >>> spr_test.attachTranslationFiles(restricted_translation, True, katie)66 >>> spr_test.attachTranslationFiles(restricted_translation, True, katie)
6767
68And the queue should have 2 entries, which exactly the same contents.68And the queue should have 2 entries, with exactly the same contents.
6969
70 >>> queue_entries = translation_import_queue.getAllEntries(70 >>> queue_entries = translation_import_queue.getAllEntries(
71 ... target=spr_test.sourcepackage)71 ... target=spr_test.sourcepackage)
@@ -78,7 +78,7 @@
78 something/en-US.xpi katie78 something/en-US.xpi katie
79 po/es.po katie79 po/es.po katie
8080
81Commit, so the uploaded traslations become available to the scripts.81Commit, so the uploaded translations become available to the scripts.
8282
83 >>> transaction.commit()83 >>> transaction.commit()
8484
8585
=== modified file 'lib/lp/translations/model/translationimportqueue.py'
--- lib/lp/translations/model/translationimportqueue.py 2011-07-26 11:22:15 +0000
+++ lib/lp/translations/model/translationimportqueue.py 2011-09-12 15:16:27 +0000
@@ -835,45 +835,43 @@
835 :return: The matching entry or None, if no matching entry was found835 :return: The matching entry or None, if no matching entry was found
836 at all."""836 at all."""
837837
838 # Find possible candidates by querying the database.838 # We disallow entries with the identical path, importer, potemplate
839 queries = ['TranslationImportQueueEntry.path = %s' % sqlvalues(path)]839 # and target (eg. productseries or distroseries/sourcepackagename).
840 queries.append(840 clauses = [
841 'TranslationImportQueueEntry.importer = %s' % sqlvalues(importer))841 TranslationImportQueueEntry.path == path,
842 # Depending on how specific the new entry is, potemplate and pofile842 TranslationImportQueueEntry.importer == importer,
843 # may be None.843 ]
844 if potemplate is not None:844 if potemplate is not None:
845 queries.append(845 clauses.append(
846 'TranslationImportQueueEntry.potemplate = %s' % sqlvalues(846 TranslationImportQueueEntry.potemplate == potemplate)
847 potemplate))
848 if pofile is not None:847 if pofile is not None:
849 queries.append(848 clauses.append(Or(
850 'TranslationImportQueueEntry.pofile = %s' % sqlvalues(pofile))849 TranslationImportQueueEntry.pofile == pofile,
851 if sourcepackagename is not None:850 TranslationImportQueueEntry.pofile == None))
852 # The import is related with a sourcepackage and a distribution.851 if productseries is None:
853 queries.append(852 assert sourcepackagename is not None and distroseries is not None
854 'TranslationImportQueueEntry.sourcepackagename = %s' % (853 clauses.extend([
855 sqlvalues(sourcepackagename)))854 (TranslationImportQueueEntry.distroseries_id ==
856 queries.append(855 distroseries.id),
857 'TranslationImportQueueEntry.distroseries = %s' % sqlvalues(856 (TranslationImportQueueEntry.sourcepackagename_id ==
858 distroseries))857 sourcepackagename.id),
858 ])
859 else:859 else:
860 # The import is related with a productseries.860 clauses.append(
861 assert productseries is not None, (861 TranslationImportQueueEntry.productseries_id ==
862 'sourcepackagename and productseries cannot be both None at'862 productseries.id)
863 ' the same time.')863 store = IMasterStore(TranslationImportQueueEntry)
864864 entries = store.find(
865 queries.append(865 TranslationImportQueueEntry, *clauses)
866 'TranslationImportQueueEntry.productseries = %s' % sqlvalues(866 entries = list(
867 productseries))867 entries.order_by(
868 # Order the results by level of specificity.868 ['pofile is null desc', 'potemplate is null desc']))
869 entries = TranslationImportQueueEntry.select(869 count = len(entries)
870 ' AND '.join(queries),
871 orderBy="potemplate IS NULL DESC, pofile IS NULL DESC")
872870
873 # Deal with the simple cases.871 # Deal with the simple cases.
874 if entries.count() == 0:872 if count == 0:
875 return None873 return None
876 if entries.count() == 1:874 if count == 1:
877 return entries[0]875 return entries[0]
878876
879 # Check that the top two entries differ in levels of specificity.877 # Check that the top two entries differ in levels of specificity.
@@ -1209,7 +1207,7 @@
1209 entry.path, entry.importer, potemplate, pofile,1207 entry.path, entry.importer, potemplate, pofile,
1210 entry.sourcepackagename, entry.distroseries, entry.productseries)1208 entry.sourcepackagename, entry.distroseries, entry.productseries)
12111209
1212 if existing_entry is None:1210 if existing_entry is None or existing_entry == entry:
1213 entry.potemplate = potemplate1211 entry.potemplate = potemplate
1214 entry.pofile = pofile1212 entry.pofile = pofile
12151213
12161214
=== modified file 'lib/lp/translations/tests/test_autoapproval.py'
--- lib/lp/translations/tests/test_autoapproval.py 2011-08-03 11:00:11 +0000
+++ lib/lp/translations/tests/test_autoapproval.py 2011-09-12 15:16:27 +0000
@@ -16,6 +16,7 @@
1616
17from pytz import UTC17from pytz import UTC
18from zope.component import getUtility18from zope.component import getUtility
19from zope.security.proxy import removeSecurityProxy
1920
20from canonical.launchpad.interfaces.lpstorm import IMasterStore21from canonical.launchpad.interfaces.lpstorm import IMasterStore
21from canonical.launchpad.webapp.testing import verifyObject22from canonical.launchpad.webapp.testing import verifyObject
@@ -1115,8 +1116,9 @@
11151116
1116 target = self._copyTargetFromEntry(same_target_as)1117 target = self._copyTargetFromEntry(same_target_as)
11171118
1118 return self.factory.makeTranslationImportQueueEntry(1119 return removeSecurityProxy(
1119 path=path, status=status, **target)1120 self.factory.makeTranslationImportQueueEntry(
1121 path=path, status=status, **target))
11201122
1121 def _makeTranslationEntry(self, path, status=None, same_target_as=None):1123 def _makeTranslationEntry(self, path, status=None, same_target_as=None):
1122 """Create an import queue entry for a translation file.1124 """Create an import queue entry for a translation file.
@@ -1126,8 +1128,9 @@
1126 entry that may have to be blocked depending on same_target_as.1128 entry that may have to be blocked depending on same_target_as.
1127 """1129 """
1128 target = self._copyTargetFromEntry(same_target_as)1130 target = self._copyTargetFromEntry(same_target_as)
1129 return self.factory.makeTranslationImportQueueEntry(1131 return removeSecurityProxy(
1130 path=path, status=status, **target)1132 self.factory.makeTranslationImportQueueEntry(
1133 path=path, status=status, **target))
11311134
1132 def test_getBlockableDirectories_checks_templates(self):1135 def test_getBlockableDirectories_checks_templates(self):
1133 old_blocklist = self.queue._getBlockableDirectories()1136 old_blocklist = self.queue._getBlockableDirectories()
@@ -1180,7 +1183,6 @@
11801183
1181 translations = self._makeTranslationEntry(1184 translations = self._makeTranslationEntry(
1182 'de.po', same_target_as=blocked_template)1185 'de.po', same_target_as=blocked_template)
1183
1184 self.assertTrue(self.queue._isBlockable(translations, blocklist))1186 self.assertTrue(self.queue._isBlockable(translations, blocklist))
11851187
1186 def test_isBlockable_multiple_blocked(self):1188 def test_isBlockable_multiple_blocked(self):
11871189
=== modified file 'lib/lp/translations/tests/test_translationimportqueue.py'
--- lib/lp/translations/tests/test_translationimportqueue.py 2011-08-03 11:00:11 +0000
+++ lib/lp/translations/tests/test_translationimportqueue.py 2011-09-12 15:16:27 +0000
@@ -429,3 +429,21 @@
429 productseries=self.productseries)429 productseries=self.productseries)
430 stripped_path = path.lstrip('/')430 stripped_path = path.lstrip('/')
431 self.assertEqual([stripped_path], self._getQueuePaths())431 self.assertEqual([stripped_path], self._getQueuePaths())
432
433 def test_addOrUpdateEntry_detects_conflicts(self):
434 pot = self.factory.makePOTemplate(translation_domain='domain')
435 uploader = self.factory.makePerson()
436 pofile = self.factory.makePOFile(potemplate=pot, language_code='fr')
437
438 # Add an import queue entry with a single pofile for a template.
439 tiqe1 = self.factory.makeTranslationImportQueueEntry(
440 path=pofile.path, productseries=pot.productseries,
441 potemplate=pot, uploader=uploader)
442
443 # Add an import queue entry for a the same pofile, but done
444 # directly on the pofile object (i.e. more specific).
445 tiqe2 = self.factory.makeTranslationImportQueueEntry(
446 path=pofile.path, productseries=pot.productseries,
447 potemplate=pot, pofile=pofile, uploader=uploader)
448
449 self.assertEquals(tiqe1, tiqe2)
432450
=== modified file 'lib/lp/translations/utilities/tests/test_translation_importer.py'
--- lib/lp/translations/utilities/tests/test_translation_importer.py 2011-09-07 11:34:06 +0000
+++ lib/lp/translations/utilities/tests/test_translation_importer.py 2011-09-12 15:16:27 +0000
@@ -259,10 +259,8 @@
259 """ % potmsgset2.msgid_singular.msgid259 """ % potmsgset2.msgid_singular.msgid
260260
261 entry = self.factory.makeTranslationImportQueueEntry(261 entry = self.factory.makeTranslationImportQueueEntry(
262 'foo.po', potemplate=template,262 'foo.po', potemplate=template, pofile=pofile,
263 status=RosettaImportStatus.APPROVED, content=text)263 status=RosettaImportStatus.APPROVED, content=text)
264 entry.pofile = pofile
265 entry.status = RosettaImportStatus.APPROVED
266 transaction.commit()264 transaction.commit()
267265
268 self.assertTrue(existing_translation.is_current_upstream)266 self.assertTrue(existing_translation.is_current_upstream)