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
1=== modified file 'lib/lp/testing/factory.py'
2--- lib/lp/testing/factory.py 2011-09-12 07:29:31 +0000
3+++ lib/lp/testing/factory.py 2011-09-12 15:16:27 +0000
4@@ -306,6 +306,9 @@
5 TranslationFileFormat,
6 )
7 from lp.translations.interfaces.translationgroup import ITranslationGroupSet
8+from lp.translations.interfaces.translationimportqueue import (
9+ ITranslationImportQueue,
10+ )
11 from lp.translations.interfaces.translationmessage import (
12 RosettaTranslationOrigin,
13 )
14@@ -314,9 +317,6 @@
15 ITranslationTemplatesBuildJobSource,
16 )
17 from lp.translations.interfaces.translator import ITranslatorSet
18-from lp.translations.model.translationimportqueue import (
19- TranslationImportQueueEntry,
20- )
21 from lp.translations.model.translationtemplateitem import (
22 TranslationTemplateItem,
23 )
24@@ -3241,9 +3241,6 @@
25
26 if content is None:
27 content = self.getUniqueString()
28- content_reference = getUtility(ILibraryFileAliasSet).create(
29- name=os.path.basename(path), size=len(content),
30- file=StringIO(content), contentType='text/plain')
31
32 if format is None:
33 format = TranslationFileFormat.PO
34@@ -3251,11 +3248,17 @@
35 if status is None:
36 status = RosettaImportStatus.NEEDS_REVIEW
37
38- return TranslationImportQueueEntry(
39- path=path, productseries=productseries, distroseries=distroseries,
40- sourcepackagename=sourcepackagename, importer=uploader,
41- content=content_reference, status=status, format=format,
42- by_maintainer=by_maintainer)
43+ if type(content) == unicode:
44+ content = content.encode('utf-8')
45+
46+ entry = getUtility(ITranslationImportQueue).addOrUpdateEntry(
47+ path=path, content=content, by_maintainer=by_maintainer,
48+ importer=uploader, productseries=productseries,
49+ distroseries=distroseries, sourcepackagename=sourcepackagename,
50+ potemplate=potemplate, pofile=pofile, format=format)
51+ entry.setStatus(
52+ status, getUtility(ILaunchpadCelebrities).rosetta_experts)
53+ return entry
54
55 def makeMailingList(self, team, owner):
56 """Create a mailing list for the team."""
57
58=== modified file 'lib/lp/translations/browser/potemplate.py'
59--- lib/lp/translations/browser/potemplate.py 2011-08-25 19:48:19 +0000
60+++ lib/lp/translations/browser/potemplate.py 2011-09-12 15:16:27 +0000
61@@ -76,6 +76,7 @@
62 from lp.registry.browser.productseries import ProductSeriesFacets
63 from lp.registry.browser.sourcepackage import SourcePackageFacets
64 from lp.registry.interfaces.productseries import IProductSeries
65+from lp.registry.interfaces.role import IPersonRoles
66 from lp.registry.interfaces.sourcepackage import ISourcePackage
67 from lp.registry.model.packaging import Packaging
68 from lp.registry.model.product import Product
69@@ -928,7 +929,9 @@
70 self.distroseries = series
71 else:
72 self.productseries = series
73- self.can_admin = check_permission('launchpad.Admin', series)
74+ user = IPersonRoles(self.user, None)
75+ self.can_admin = (user is not None and
76+ (user.in_admin or user.in_rosetta_experts))
77 self.can_edit = (
78 self.can_admin or
79 check_permission('launchpad.TranslationsAdmin', series))
80
81=== modified file 'lib/lp/translations/doc/sourcepackagerelease-translations.txt'
82--- lib/lp/translations/doc/sourcepackagerelease-translations.txt 2011-05-27 19:53:20 +0000
83+++ lib/lp/translations/doc/sourcepackagerelease-translations.txt 2011-09-12 15:16:27 +0000
84@@ -65,7 +65,7 @@
85
86 >>> spr_test.attachTranslationFiles(restricted_translation, True, katie)
87
88-And the queue should have 2 entries, which exactly the same contents.
89+And the queue should have 2 entries, with exactly the same contents.
90
91 >>> queue_entries = translation_import_queue.getAllEntries(
92 ... target=spr_test.sourcepackage)
93@@ -78,7 +78,7 @@
94 something/en-US.xpi katie
95 po/es.po katie
96
97-Commit, so the uploaded traslations become available to the scripts.
98+Commit, so the uploaded translations become available to the scripts.
99
100 >>> transaction.commit()
101
102
103=== modified file 'lib/lp/translations/model/translationimportqueue.py'
104--- lib/lp/translations/model/translationimportqueue.py 2011-07-26 11:22:15 +0000
105+++ lib/lp/translations/model/translationimportqueue.py 2011-09-12 15:16:27 +0000
106@@ -835,45 +835,43 @@
107 :return: The matching entry or None, if no matching entry was found
108 at all."""
109
110- # Find possible candidates by querying the database.
111- queries = ['TranslationImportQueueEntry.path = %s' % sqlvalues(path)]
112- queries.append(
113- 'TranslationImportQueueEntry.importer = %s' % sqlvalues(importer))
114- # Depending on how specific the new entry is, potemplate and pofile
115- # may be None.
116+ # We disallow entries with the identical path, importer, potemplate
117+ # and target (eg. productseries or distroseries/sourcepackagename).
118+ clauses = [
119+ TranslationImportQueueEntry.path == path,
120+ TranslationImportQueueEntry.importer == importer,
121+ ]
122 if potemplate is not None:
123- queries.append(
124- 'TranslationImportQueueEntry.potemplate = %s' % sqlvalues(
125- potemplate))
126+ clauses.append(
127+ TranslationImportQueueEntry.potemplate == potemplate)
128 if pofile is not None:
129- queries.append(
130- 'TranslationImportQueueEntry.pofile = %s' % sqlvalues(pofile))
131- if sourcepackagename is not None:
132- # The import is related with a sourcepackage and a distribution.
133- queries.append(
134- 'TranslationImportQueueEntry.sourcepackagename = %s' % (
135- sqlvalues(sourcepackagename)))
136- queries.append(
137- 'TranslationImportQueueEntry.distroseries = %s' % sqlvalues(
138- distroseries))
139+ clauses.append(Or(
140+ TranslationImportQueueEntry.pofile == pofile,
141+ TranslationImportQueueEntry.pofile == None))
142+ if productseries is None:
143+ assert sourcepackagename is not None and distroseries is not None
144+ clauses.extend([
145+ (TranslationImportQueueEntry.distroseries_id ==
146+ distroseries.id),
147+ (TranslationImportQueueEntry.sourcepackagename_id ==
148+ sourcepackagename.id),
149+ ])
150 else:
151- # The import is related with a productseries.
152- assert productseries is not None, (
153- 'sourcepackagename and productseries cannot be both None at'
154- ' the same time.')
155-
156- queries.append(
157- 'TranslationImportQueueEntry.productseries = %s' % sqlvalues(
158- productseries))
159- # Order the results by level of specificity.
160- entries = TranslationImportQueueEntry.select(
161- ' AND '.join(queries),
162- orderBy="potemplate IS NULL DESC, pofile IS NULL DESC")
163+ clauses.append(
164+ TranslationImportQueueEntry.productseries_id ==
165+ productseries.id)
166+ store = IMasterStore(TranslationImportQueueEntry)
167+ entries = store.find(
168+ TranslationImportQueueEntry, *clauses)
169+ entries = list(
170+ entries.order_by(
171+ ['pofile is null desc', 'potemplate is null desc']))
172+ count = len(entries)
173
174 # Deal with the simple cases.
175- if entries.count() == 0:
176+ if count == 0:
177 return None
178- if entries.count() == 1:
179+ if count == 1:
180 return entries[0]
181
182 # Check that the top two entries differ in levels of specificity.
183@@ -1209,7 +1207,7 @@
184 entry.path, entry.importer, potemplate, pofile,
185 entry.sourcepackagename, entry.distroseries, entry.productseries)
186
187- if existing_entry is None:
188+ if existing_entry is None or existing_entry == entry:
189 entry.potemplate = potemplate
190 entry.pofile = pofile
191
192
193=== modified file 'lib/lp/translations/tests/test_autoapproval.py'
194--- lib/lp/translations/tests/test_autoapproval.py 2011-08-03 11:00:11 +0000
195+++ lib/lp/translations/tests/test_autoapproval.py 2011-09-12 15:16:27 +0000
196@@ -16,6 +16,7 @@
197
198 from pytz import UTC
199 from zope.component import getUtility
200+from zope.security.proxy import removeSecurityProxy
201
202 from canonical.launchpad.interfaces.lpstorm import IMasterStore
203 from canonical.launchpad.webapp.testing import verifyObject
204@@ -1115,8 +1116,9 @@
205
206 target = self._copyTargetFromEntry(same_target_as)
207
208- return self.factory.makeTranslationImportQueueEntry(
209- path=path, status=status, **target)
210+ return removeSecurityProxy(
211+ self.factory.makeTranslationImportQueueEntry(
212+ path=path, status=status, **target))
213
214 def _makeTranslationEntry(self, path, status=None, same_target_as=None):
215 """Create an import queue entry for a translation file.
216@@ -1126,8 +1128,9 @@
217 entry that may have to be blocked depending on same_target_as.
218 """
219 target = self._copyTargetFromEntry(same_target_as)
220- return self.factory.makeTranslationImportQueueEntry(
221- path=path, status=status, **target)
222+ return removeSecurityProxy(
223+ self.factory.makeTranslationImportQueueEntry(
224+ path=path, status=status, **target))
225
226 def test_getBlockableDirectories_checks_templates(self):
227 old_blocklist = self.queue._getBlockableDirectories()
228@@ -1180,7 +1183,6 @@
229
230 translations = self._makeTranslationEntry(
231 'de.po', same_target_as=blocked_template)
232-
233 self.assertTrue(self.queue._isBlockable(translations, blocklist))
234
235 def test_isBlockable_multiple_blocked(self):
236
237=== modified file 'lib/lp/translations/tests/test_translationimportqueue.py'
238--- lib/lp/translations/tests/test_translationimportqueue.py 2011-08-03 11:00:11 +0000
239+++ lib/lp/translations/tests/test_translationimportqueue.py 2011-09-12 15:16:27 +0000
240@@ -429,3 +429,21 @@
241 productseries=self.productseries)
242 stripped_path = path.lstrip('/')
243 self.assertEqual([stripped_path], self._getQueuePaths())
244+
245+ def test_addOrUpdateEntry_detects_conflicts(self):
246+ pot = self.factory.makePOTemplate(translation_domain='domain')
247+ uploader = self.factory.makePerson()
248+ pofile = self.factory.makePOFile(potemplate=pot, language_code='fr')
249+
250+ # Add an import queue entry with a single pofile for a template.
251+ tiqe1 = self.factory.makeTranslationImportQueueEntry(
252+ path=pofile.path, productseries=pot.productseries,
253+ potemplate=pot, uploader=uploader)
254+
255+ # Add an import queue entry for a the same pofile, but done
256+ # directly on the pofile object (i.e. more specific).
257+ tiqe2 = self.factory.makeTranslationImportQueueEntry(
258+ path=pofile.path, productseries=pot.productseries,
259+ potemplate=pot, pofile=pofile, uploader=uploader)
260+
261+ self.assertEquals(tiqe1, tiqe2)
262
263=== modified file 'lib/lp/translations/utilities/tests/test_translation_importer.py'
264--- lib/lp/translations/utilities/tests/test_translation_importer.py 2011-09-07 11:34:06 +0000
265+++ lib/lp/translations/utilities/tests/test_translation_importer.py 2011-09-12 15:16:27 +0000
266@@ -259,10 +259,8 @@
267 """ % potmsgset2.msgid_singular.msgid
268
269 entry = self.factory.makeTranslationImportQueueEntry(
270- 'foo.po', potemplate=template,
271+ 'foo.po', potemplate=template, pofile=pofile,
272 status=RosettaImportStatus.APPROVED, content=text)
273- entry.pofile = pofile
274- entry.status = RosettaImportStatus.APPROVED
275 transaction.commit()
276
277 self.assertTrue(existing_translation.is_current_upstream)