← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~wgrant/launchpad/bug-1186610 into lp:launchpad

 

William Grant has proposed merging lp:~wgrant/launchpad/bug-1186610 into lp:launchpad.

Commit message:
Stop crashing on unusual % sequences in translation message IDs and context strings.

Requested reviews:
  William Grant (wgrant): code
Related bugs:
  Bug #1186610 in Launchpad itself: "Uploading .PO or .POT file gives "tuple index out of range" error"
  https://bugs.launchpad.net/launchpad/+bug/1186610

For more details, see:
https://code.launchpad.net/~wgrant/launchpad/bug-1186610/+merge/191939

Port most of lp.translations.model.potemplate to use native Storm rather than the SQLObject compat layer. It was previously using sqlvalues() in such a way that it could trip over unusual % sequences in translation message IDs or context strings.
-- 
https://code.launchpad.net/~wgrant/launchpad/bug-1186610/+merge/191939
Your team Launchpad code reviewers is subscribed to branch lp:launchpad.
=== modified file 'lib/lp/translations/browser/potemplate.py'
--- lib/lp/translations/browser/potemplate.py	2013-01-07 02:40:55 +0000
+++ lib/lp/translations/browser/potemplate.py	2013-10-21 06:11:14 +0000
@@ -62,6 +62,7 @@
 from lp.registry.model.productseries import ProductSeries
 from lp.registry.model.sourcepackagename import SourcePackageName
 from lp.services.helpers import is_tar_filename
+from lp.services.propertycache import cachedproperty
 from lp.services.webapp import (
     canonical_url,
     enabled_with_permission,
@@ -309,7 +310,7 @@
         return (translation_group is not None and
                 translation_group.translation_guide_url is not None)
 
-    @property
+    @cachedproperty
     def related_templates_by_source(self):
         by_source = list(
             self.context.relatives_by_source[:self.SHOW_RELATED_TEMPLATES])

=== modified file 'lib/lp/translations/interfaces/potemplate.py'
--- lib/lp/translations/interfaces/potemplate.py	2013-01-07 02:40:55 +0000
+++ lib/lp/translations/interfaces/potemplate.py	2013-10-21 06:11:14 +0000
@@ -667,9 +667,6 @@
     def __iter__():
         """Return an iterator over all PO templates."""
 
-    def getByIDs(ids):
-        """Return all PO templates with the given IDs."""
-
     def getAllByName(name):
         """Return a list with all PO templates with the given name."""
 

=== modified file 'lib/lp/translations/model/potemplate.py'
--- lib/lp/translations/model/potemplate.py	2013-06-20 05:50:00 +0000
+++ lib/lp/translations/model/potemplate.py	2013-10-21 06:11:14 +0000
@@ -36,6 +36,7 @@
     LeftJoin,
     Or,
     Select,
+    SQL,
     )
 from storm.info import ClassAlias
 from storm.store import (
@@ -55,6 +56,7 @@
 from lp.registry.interfaces.person import validate_public_person
 from lp.registry.model.packaging import Packaging
 from lp.registry.model.sourcepackagename import SourcePackageName
+from lp.services.database.bulk import load_related
 from lp.services.database.collection import Collection
 from lp.services.database.constants import DEFAULT
 from lp.services.database.datetimecol import UtcDateTimeCol
@@ -66,9 +68,7 @@
     )
 from lp.services.database.sqlbase import (
     flush_database_updates,
-    quote,
     SQLBase,
-    sqlvalues,
     )
 from lp.services.helpers import shortlist
 from lp.services.mail.helpers import get_email_template
@@ -362,31 +362,29 @@
     def relatives_by_source(self):
         """See `IPOTemplate`."""
         if self.productseries is not None:
-            return POTemplate.select(
-                'id <> %s AND productseries = %s AND iscurrent' % sqlvalues(
-                    self, self.productseries), orderBy=['name'])
+            return Store.of(self).find(
+                POTemplate,
+                POTemplate.id != self.id,
+                POTemplate.productseriesID == self.productseries.id,
+                POTemplate.iscurrent).order_by(POTemplate.name)
         elif (self.distroseries is not None and
               self.sourcepackagename is not None):
-            return POTemplate.select('''
-                id <> %s AND
-                distroseries = %s AND
-                sourcepackagename = %s AND
-                iscurrent
-                ''' % sqlvalues(
-                    self, self.distroseries, self.sourcepackagename),
-                orderBy=['name'])
+            return Store.of(self).find(
+                POTemplate,
+                POTemplate.id != self.id,
+                POTemplate.distroseriesID == self.distroseries.id,
+                POTemplate.sourcepackagenameID == self.sourcepackagename.id,
+                POTemplate.iscurrent).order_by(POTemplate.name)
         else:
             raise AssertionError('Unknown POTemplate source.')
 
     @property
     def language_count(self):
-        return Language.select('''
-            POFile.language = Language.id AND
-            POFile.currentcount + POFile.rosettacount > 0 AND
-            POFile.potemplate = %s
-            ''' % sqlvalues(self.id),
-            clauseTables=['POFile'],
-            distinct=True).count()
+        return IStore(Language).find(
+            Language,
+            POFile.language == Language.id,
+            POFile.currentcount + POFile.rosettacount > 0,
+            POFile.potemplateID == self.id).config(distinct=True).count()
 
     @cachedproperty
     def sourcepackage(self):
@@ -421,70 +419,62 @@
         """Return SQL clauses for finding POTMsgSets which belong
         to this POTemplate."""
         clauses = [
-            'TranslationTemplateItem.potemplate = %s' % sqlvalues(self),
-            'TranslationTemplateItem.potmsgset = POTMsgSet.id',
+            TranslationTemplateItem.potemplateID == self.id,
+            TranslationTemplateItem.potmsgsetID == POTMsgSet.id,
             ]
         return clauses
 
     def getPOTMsgSetByMsgIDText(self, singular_text, plural_text=None,
                                 only_current=False, context=None):
         """See `IPOTemplate`."""
-        clauses = self._getPOTMsgSetSelectionClauses()
-        if only_current:
-            clauses.append('TranslationTemplateItem.sequence > 0')
-        if context is not None:
-            clauses.append('context = %s' % sqlvalues(context))
-        else:
-            clauses.append('context IS NULL')
-
         # Find a message ID with the given text.
         try:
             singular_msgid = POMsgID.byMsgid(singular_text)
         except SQLObjectNotFound:
             return None
-        clauses.append('msgid_singular = %s' % sqlvalues(singular_msgid))
 
         # Find a message ID for the plural string.
+        plural_msgid = None
         if plural_text is not None:
             try:
                 plural_msgid = POMsgID.byMsgid(plural_text)
-                clauses.append('msgid_plural = %s' % sqlvalues(plural_msgid))
             except SQLObjectNotFound:
                 return None
-        else:
-            # You have to be explicit now.
-            clauses.append('msgid_plural IS NULL')
 
         # Find a message set with the given message ID.
-        return POTMsgSet.selectOne(' AND '.join(clauses),
-                                   clauseTables=['TranslationTemplateItem'])
+        clauses = self._getPOTMsgSetSelectionClauses()
+        clauses.extend([
+            POTMsgSet.msgid_singular == singular_msgid,
+            POTMsgSet.msgid_plural == plural_msgid,
+            POTMsgSet.context == context,
+            ])
+        if only_current:
+            clauses.append(TranslationTemplateItem.sequence > 0)
+        return Store.of(self).find(POTMsgSet, *clauses).one()
 
     def getPOTMsgSetBySequence(self, sequence):
         """See `IPOTemplate`."""
         assert sequence > 0, ('%r is out of range')
-
         clauses = self._getPOTMsgSetSelectionClauses()
-        clauses.append(
-            'TranslationTemplateItem.sequence = %s' % sqlvalues(sequence))
-
-        return POTMsgSet.selectOne(' AND '.join(clauses),
-                                   clauseTables=['TranslationTemplateItem'])
+        clauses.append(TranslationTemplateItem.sequence == sequence)
+        return Store.of(self).find(POTMsgSet, *clauses).one()
 
     def getPOTMsgSets(self, current=True, prefetch=True):
         """See `IPOTemplate`."""
         clauses = self._getPOTMsgSetSelectionClauses()
-
         if current:
             # Only count the number of POTMsgSet that are current.
-            clauses.append('TranslationTemplateItem.sequence > 0')
+            clauses.append(TranslationTemplateItem.sequence > 0)
+        result = Store.of(self).find(
+            POTMsgSet, *clauses).order_by(TranslationTemplateItem.sequence)
 
-        query = POTMsgSet.select(" AND ".join(clauses),
-                                 clauseTables=['TranslationTemplateItem'],
-                                 orderBy=['TranslationTemplateItem.sequence'])
         if prefetch:
-            query = query.prejoin(['msgid_singular', 'msgid_plural'])
-
-        return query
+            def prefetch_msgids(rows):
+                load_related(
+                    POMsgID, rows, ['msgid_singularID', 'msgid_pluralID'])
+            return DecoratedResultSet(result, pre_iter_hook=prefetch_msgids)
+        else:
+            return result
 
     def getTranslationCredits(self):
         """See `IPOTemplate`."""
@@ -511,18 +501,16 @@
     def getPOTMsgSetByID(self, id):
         """See `IPOTemplate`."""
         clauses = self._getPOTMsgSetSelectionClauses()
-        clauses.append('POTMsgSet.id = %s' % sqlvalues(id))
-
-        return POTMsgSet.selectOne(' AND '.join(clauses),
-                                   clauseTables=['TranslationTemplateItem'])
+        clauses.append(POTMsgSet.id == id)
+        return Store.of(self).find(POTMsgSet, *clauses).one()
 
     def languages(self):
         """See `IPOTemplate`."""
-        return Language.select("POFile.language = Language.id AND "
-                               "Language.code <> 'en' AND "
-                               "POFile.potemplate = %d" % self.id,
-                               clauseTables=['POFile', 'Language'],
-                               distinct=True)
+        return Store.of(self).find(
+            Language,
+            POFile.languageID == Language.id,
+            Language.code != u'en',
+            POFile.potemplateID == self.id).config(distinct=True)
 
     def getPOFileByPath(self, path):
         """See `IPOTemplate`."""
@@ -538,15 +526,10 @@
             # the usual get() followed by "is None"; the dict may contain None
             # values to indicate we looked for a POFile and found none.
             return self._cached_pofiles_by_language[language_code]
-
-        self._cached_pofiles_by_language[language_code] = POFile.selectOne("""
-            POFile.potemplate = %d AND
-            POFile.language = Language.id AND
-            Language.code = %s
-            """ % (self.id,
-                   quote(language_code)),
-            clauseTables=['Language'])
-
+        self._cached_pofiles_by_language[language_code] = Store.of(self).find(
+            POFile,
+            POFile.potemplateID == self.id, POFile.languageID == Language.id,
+            Language.code == language_code).one()
         return self._cached_pofiles_by_language[language_code]
 
     def getOtherSidePOTemplate(self):
@@ -608,12 +591,6 @@
         else:
             pofile.unreviewedCount()
 
-    def _null_quote(self, value):
-        if value is None:
-            return " IS NULL "
-        else:
-            return " = %s" % sqlvalues(value)
-
     def _getPOTMsgSetBy(self, msgid_singular, msgid_plural=None, context=None,
                         sharing_templates=False):
         """Look for a POTMsgSet by msgid_singular, msgid_plural, context.
@@ -622,35 +599,23 @@
         POTMsgSet, look through sharing templates as well.
         """
         clauses = [
-            'TranslationTemplateItem.potmsgset = POTMsgSet.id',
+            TranslationTemplateItem.potmsgsetID == POTMsgSet.id,
+            POTMsgSet.msgid_singular == msgid_singular,
+            POTMsgSet.msgid_plural == msgid_plural,
+            POTMsgSet.context == context,
             ]
-        clause_tables = ['TranslationTemplateItem']
         if sharing_templates:
             clauses.append(
-                'TranslationTemplateItem.potemplate in %s' % sqlvalues(
-                    self._sharing_ids))
-        else:
-            clauses.append(
-                'TranslationTemplateItem.potemplate = %s' % sqlvalues(self))
-
-        clauses.append(
-            'POTMsgSet.msgid_singular %s' % self._null_quote(msgid_singular))
-        clauses.append(
-            'POTMsgSet.msgid_plural %s' % self._null_quote(msgid_plural))
-        clauses.append(
-            'POTMsgSet.context %s' % self._null_quote(context))
-
-        result = POTMsgSet.select(
-            ' AND '.join(clauses),
-            clauseTables=clause_tables,
-            # If there are multiple messages, make the one from the
-            # current POTemplate be returned first.
-            orderBy=['(TranslationTemplateItem.POTemplate<>%s)' % (
-                sqlvalues(self))])[:2]
-        if not result.is_empty():
-            return result[0]
-        else:
-            return None
+                TranslationTemplateItem.potemplateID.is_in(self._sharing_ids))
+        else:
+            clauses.append(TranslationTemplateItem.potemplateID == self.id)
+
+        # If there are multiple messages, make the one from the
+        # current POTemplate be returned first.
+        order_by = SQL(
+            'TranslationTemplateItem.potemplate <> ?', params=(self.id,))
+        return IStore(POTMsgSet).find(
+            POTMsgSet, *clauses).order_by(order_by).first()
 
     def hasMessageID(self, msgid_singular, msgid_plural, context=None):
         """See `IPOTemplate`."""
@@ -660,11 +625,8 @@
     def hasPluralMessage(self):
         """See `IPOTemplate`."""
         clauses = self._getPOTMsgSetSelectionClauses()
-        clauses.append(
-            'POTMsgSet.msgid_plural IS NOT NULL')
-        return bool(POTMsgSet.select(
-                ' AND '.join(clauses),
-                clauseTables=['TranslationTemplateItem']))
+        clauses.append(POTMsgSet.msgid_plural != None)
+        return not Store.of(self).find(POTMsgSet, *clauses).is_empty()
 
     def export(self):
         """See `IPOTemplate`."""
@@ -1294,13 +1256,6 @@
         for potemplate in res:
             yield potemplate
 
-    def getByIDs(self, ids):
-        """See `IPOTemplateSet`."""
-        values = ",".join(sqlvalues(*ids))
-        return POTemplate.select("POTemplate.id in (%s)" % values,
-            prejoins=["productseries", "distroseries", "sourcepackagename"],
-            orderBy=["POTemplate.id"])
-
     def getAllByName(self, name):
         """See `IPOTemplateSet`."""
         return POTemplate.selectBy(name=name, orderBy=['name', 'id'])
@@ -1401,7 +1356,7 @@
         """See `IPOTemplateSet`."""
         rowcount = IMasterStore(POTemplate).execute(
             "DELETE FROM SuggestivePOTemplate "
-            "WHERE potemplate = %s" % sqlvalues(potemplate)).rowcount
+            "WHERE potemplate = ?", params=(potemplate.id,)).rowcount
         return rowcount == 1
 
     def populateSuggestivePOTemplatesCache(self):
@@ -1420,13 +1375,14 @@
                     Product.id = ProductSeries.product
                 WHERE
                     POTemplate.iscurrent AND (
-                        Distribution.translations_usage IN %(usage)s OR
-                        Product.translations_usage IN %(usage)s)
+                        Distribution.translations_usage IN (?, ?) OR
+                        Product.translations_usage IN (?, ?))
                 ORDER BY POTemplate.id
             )
-            """ % {
-                'usage': sqlvalues(
-                    ServiceUsage.LAUNCHPAD, ServiceUsage.EXTERNAL)}
+            """,
+            params=(
+                ServiceUsage.LAUNCHPAD.value, ServiceUsage.EXTERNAL.value,
+                ServiceUsage.LAUNCHPAD.value, ServiceUsage.EXTERNAL.value)
         ).rowcount
 
 
@@ -1569,8 +1525,8 @@
         if name_pattern is None:
             templatename_clause = True
         else:
-            templatename_clause = (
-                "potemplate.name ~ %s" % sqlvalues(name_pattern))
+            templatename_clause = SQL(
+                "POTemplate.name ~ ?", params=(name_pattern,))
 
         return self._queryPOTemplates(templatename_clause)
 

=== modified file 'lib/lp/translations/templates/potemplate-index.pt'
--- lib/lp/translations/templates/potemplate-index.pt	2012-08-06 06:09:19 +0000
+++ lib/lp/translations/templates/potemplate-index.pt	2013-10-21 06:11:14 +0000
@@ -83,7 +83,7 @@
 
     <div class="yui-u">
       <div class="portlet"
-           tal:condition="context/relatives_by_source">
+           tal:condition="view/related_templates_by_source">
         <h3>Related templates</h3>
         <p id="potemplate-relatives">
           <span>Other templates here:</span>

=== modified file 'lib/lp/translations/tests/test_potemplate.py'
--- lib/lp/translations/tests/test_potemplate.py	2012-11-01 15:35:53 +0000
+++ lib/lp/translations/tests/test_potemplate.py	2013-10-21 06:11:14 +0000
@@ -357,7 +357,7 @@
         subset = getUtility(IPOTemplateSet).getSharingSubset(
             distribution=self.ubuntu)
         classes = subset.groupEquivalentPOTemplates(
-            name_pattern='krungthepmahanakorn.*-etc')
+            name_pattern=u'krungthepmahanakorn.*-etc')
 
         expected = {
             (unique_name, self.package.name): [bangkok_template],

=== modified file 'lib/lp/translations/tests/test_shared_potemplate.py'
--- lib/lp/translations/tests/test_shared_potemplate.py	2013-01-07 02:40:55 +0000
+++ lib/lp/translations/tests/test_shared_potemplate.py	2013-10-21 06:11:14 +0000
@@ -242,13 +242,13 @@
         # Baseline test.
         self.assertContentEqual(
             ['foo', 'foo-bar', 'foo-two'],
-            self._makeAndFind(['foo', 'foo-bar', 'foo-two'], 'foo.*'))
+            self._makeAndFind(['foo', 'foo-bar', 'foo-two'], u'foo.*'))
 
     def test_getSharingPOTemplatesByRegex_not_all(self):
         # A template may not match.
         self.assertContentEqual(
             ['foo-bar', 'foo-two'],
-            self._makeAndFind(['foo', 'foo-bar', 'foo-two'], 'foo-.*'))
+            self._makeAndFind(['foo', 'foo-bar', 'foo-two'], u'foo-.*'))
 
     def test_getSharingPOTemplatesByRegex_all(self):
         # Not passing a pattern returns all templates.
@@ -260,19 +260,19 @@
         # A not matching pattern returns no templates.
         self.assertContentEqual(
             [],
-            self._makeAndFind(['foo', 'foo-bar', 'foo-two'], "doo.+dle"))
+            self._makeAndFind(['foo', 'foo-bar', 'foo-two'], u"doo.+dle"))
 
     def test_getSharingPOTemplatesByRegex_robustness_single_quotes(self):
         # Single quotes do not confuse the regex match.
         self.assertContentEqual(
             [],
-            self._makeAndFind(['foo', 'foo-bar', 'foo-two'], "'"))
+            self._makeAndFind(['foo', 'foo-bar', 'foo-two'], u"'"))
 
     def test_getSharingPOTemplatesByRegex_robustness_double_quotes(self):
         # Double quotes do not confuse the regex match.
         self.assertContentEqual(
             [],
-            self._makeAndFind(['foo', 'foo-bar', 'foo-two'], '"'))
+            self._makeAndFind(['foo', 'foo-bar', 'foo-two'], u'"'))
 
     def test_getSharingPOTemplatesByRegex_robustness_backslash(self):
         # A backslash at the end could escape enclosing quotes without
@@ -282,7 +282,7 @@
         product = self.factory.makeProduct()
         subset = getUtility(IPOTemplateSet).getSharingSubset(product=product)
         self.assertRaises(
-            DataError, list, subset.getSharingPOTemplatesByRegex("foo.*\\"))
+            DataError, list, subset.getSharingPOTemplatesByRegex(u"foo.*\\"))
 
 
 class TestMessageSharingProductPackage(TestCaseWithFactory):

=== modified file 'lib/lp/translations/translationmerger.py'
--- lib/lp/translations/translationmerger.py	2013-06-20 05:50:00 +0000
+++ lib/lp/translations/translationmerger.py	2013-10-21 06:11:14 +0000
@@ -274,7 +274,7 @@
                 product=product, distribution=distribution,
                 sourcepackagename=sourcepackagename)
         equivalence_classes = subset.groupEquivalentPOTemplates(
-                                                self.options.template_names)
+            self.options.template_names.decode('utf-8'))
 
         class_count = len(equivalence_classes)
         log.info("Merging %d template equivalence classes." % class_count)


References