← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~cjwatson/launchpad:stormify-translationimportqueueentry into launchpad:master

 

Colin Watson has proposed merging ~cjwatson/launchpad:stormify-translationimportqueueentry into launchpad:master.

Commit message:
Convert TranslationImportQueueEntry to Storm

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/+git/launchpad/+merge/385638
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~cjwatson/launchpad:stormify-translationimportqueueentry into launchpad:master.
diff --git a/lib/lp/services/database/stormexpr.py b/lib/lp/services/database/stormexpr.py
index 38bc533..37772f1 100644
--- a/lib/lp/services/database/stormexpr.py
+++ b/lib/lp/services/database/stormexpr.py
@@ -16,6 +16,8 @@ __all__ = [
     'Greatest',
     'get_where_for_reference',
     'IsDistinctFrom',
+    'IsFalse',
+    'IsTrue',
     'NullCount',
     'NullsFirst',
     'NullsLast',
@@ -212,6 +214,26 @@ class ArrayIntersects(CompoundOper):
     oper = "&&"
 
 
+class IsTrue(SuffixExpr):
+    """True iff the input Boolean expression is `TRUE`.
+
+    Unlike `expr` or `expr == True`, this returns `FALSE` when
+    `expr IS NULL`.
+    """
+    __slots__ = ()
+    suffix = "IS TRUE"
+
+
+class IsFalse(SuffixExpr):
+    """True iff the input Boolean expression is `FALSE`.
+
+    Unlike `Not(expr)` or `expr == False`, this returns `FALSE` when
+    `expr IS NULL`.
+    """
+    __slots__ = ()
+    suffix = "IS FALSE"
+
+
 class IsDistinctFrom(CompoundOper):
     """True iff the left side is distinct from the right side."""
     __slots__ = ()
diff --git a/lib/lp/testing/factory.py b/lib/lp/testing/factory.py
index c04041d..d1cb161 100644
--- a/lib/lp/testing/factory.py
+++ b/lib/lp/testing/factory.py
@@ -3417,7 +3417,7 @@ class BareLaunchpadObjectFactory(ObjectFactory):
                                         by_maintainer=False):
         """Create a `TranslationImportQueueEntry`."""
         if path is None:
-            path = self.getUniqueString() + '.pot'
+            path = self.getUniqueUnicode() + '.pot'
 
         for_distro = not (distroseries is None and sourcepackagename is None)
         for_project = productseries is not None
@@ -3441,7 +3441,7 @@ class BareLaunchpadObjectFactory(ObjectFactory):
             uploader = self.makePerson()
 
         if content is None:
-            content = self.getUniqueString()
+            content = self.getUniqueBytes()
 
         if format is None:
             format = TranslationFileFormat.PO
@@ -3449,8 +3449,7 @@ class BareLaunchpadObjectFactory(ObjectFactory):
         if status is None:
             status = RosettaImportStatus.NEEDS_REVIEW
 
-        if type(content) == unicode:
-            content = content.encode('utf-8')
+        content = six.ensure_binary(content)
 
         entry = getUtility(ITranslationImportQueue).addOrUpdateEntry(
             path=path, content=content, by_maintainer=by_maintainer,
diff --git a/lib/lp/translations/browser/hastranslationimports.py b/lib/lp/translations/browser/hastranslationimports.py
index fe55038..c73bd82 100644
--- a/lib/lp/translations/browser/hastranslationimports.py
+++ b/lib/lp/translations/browser/hastranslationimports.py
@@ -3,6 +3,8 @@
 
 """Browser view for IHasTranslationImports."""
 
+from __future__ import absolute_import, print_function, unicode_literals
+
 __metaclass__ = type
 
 __all__ = [
diff --git a/lib/lp/translations/browser/tests/pofile-views.txt b/lib/lp/translations/browser/tests/pofile-views.txt
index 5d504d2..2221e92 100644
--- a/lib/lp/translations/browser/tests/pofile-views.txt
+++ b/lib/lp/translations/browser/tests/pofile-views.txt
@@ -403,7 +403,7 @@ Get it and check that some attributes are set as they should.
 
     >>> from lp.translations.enums import RosettaImportStatus
     >>> entry = translationimportqueue.getAllEntries(
-    ...     import_status=RosettaImportStatus.NEEDS_REVIEW)[-1]
+    ...     import_status=RosettaImportStatus.NEEDS_REVIEW).last()
     >>> entry.pofile == pofile_es
     True
 
diff --git a/lib/lp/translations/browser/tests/potemplate-views.txt b/lib/lp/translations/browser/tests/potemplate-views.txt
index 5ada83e..712ca5a 100644
--- a/lib/lp/translations/browser/tests/potemplate-views.txt
+++ b/lib/lp/translations/browser/tests/potemplate-views.txt
@@ -69,7 +69,7 @@ the entry should be linked with the IPOTemplate we are using.
 
     >>> from lp.translations.enums import RosettaImportStatus
     >>> entry = translationimportqueue.getAllEntries(
-    ...     import_status=RosettaImportStatus.NEEDS_REVIEW)[-1]
+    ...     import_status=RosettaImportStatus.NEEDS_REVIEW).last()
     >>> entry.potemplate == potemplate
     True
 
@@ -102,7 +102,7 @@ Get it and check that some attributes are set as they should. For instance,
 the entry should be linked with the IPOTemplate we are using.
 
     >>> entry = translationimportqueue.getAllEntries(
-    ...     import_status=RosettaImportStatus.NEEDS_REVIEW)[-1]
+    ...     import_status=RosettaImportStatus.NEEDS_REVIEW).last()
     >>> entry.potemplate == potemplate
     True
 
diff --git a/lib/lp/translations/browser/tests/test_translationimportqueueentry.py b/lib/lp/translations/browser/tests/test_translationimportqueueentry.py
index ce9e44d..f054931 100644
--- a/lib/lp/translations/browser/tests/test_translationimportqueueentry.py
+++ b/lib/lp/translations/browser/tests/test_translationimportqueueentry.py
@@ -3,6 +3,8 @@
 
 """Unit tests for translation import queue views."""
 
+from __future__ import absolute_import, print_function, unicode_literals
+
 from datetime import datetime
 
 from pytz import timezone
@@ -58,8 +60,8 @@ class TestTranslationImportQueueEntryView(WithScenarios, TestCaseWithFactory):
     def _makeEntry(self, productseries=None, distroseries=None,
                    sourcepackagename=None, filename=None, potemplate=None):
         if filename is None:
-            filename = self.factory.getUniqueString() + '.pot'
-        contents = self.factory.getUniqueString()
+            filename = self.factory.getUniqueUnicode() + '.pot'
+        contents = self.factory.getUniqueBytes()
         entry = self.queue.addOrUpdateEntry(
             filename, contents, False, self.uploader,
             productseries=productseries, distroseries=distroseries,
diff --git a/lib/lp/translations/doc/translationimportqueue.txt b/lib/lp/translations/doc/translationimportqueue.txt
index c62423b..1c950a6 100644
--- a/lib/lp/translations/doc/translationimportqueue.txt
+++ b/lib/lp/translations/doc/translationimportqueue.txt
@@ -878,8 +878,8 @@ We need to attach a new entry to play with:
 
 When we just import it, this method tells us that it's "just requested"
 
-    >>> entry.getElapsedTimeText()
-    'just requested'
+    >>> print(entry.getElapsedTimeText())
+    just requested
 
 Now, we need to update the 'dateimported' field to check that we get a good
 value when takes more time since the import. We need to force the date here
@@ -900,8 +900,8 @@ Let's change the field with a date 2 days, 13 hours and 5 minutes ago.
 
 And this method gets the right text.
 
-    >>> entry.getElapsedTimeText()
-    '2 days 13 hours 5 minutes ago'
+    >>> print(entry.getElapsedTimeText())
+    2 days 13 hours 5 minutes ago
 
 
 TranslationImportQueue
diff --git a/lib/lp/translations/model/pofile.py b/lib/lp/translations/model/pofile.py
index 01187aa..a975b67 100644
--- a/lib/lp/translations/model/pofile.py
+++ b/lib/lp/translations/model/pofile.py
@@ -15,6 +15,7 @@ __all__ = [
 import datetime
 
 import pytz
+import six
 from sqlobject import (
     BoolCol,
     ForeignKey,
@@ -981,7 +982,7 @@ class POFile(SQLBase, POFileMixIn):
             template_mail = 'poimport-not-exported-from-rosetta.txt'
             import_rejected = True
             entry_to_import.setErrorOutput(
-                "File was not exported from Launchpad.")
+                u"File was not exported from Launchpad.")
         except (MixedNewlineMarkersError, TranslationFormatSyntaxError,
                 TranslationFormatInvalidInputError,
                 UnicodeDecodeError) as exception:
@@ -995,7 +996,7 @@ class POFile(SQLBase, POFileMixIn):
             else:
                 template_mail = 'poimport-syntax-error.txt'
             import_rejected = True
-            error_text = str(exception)
+            error_text = six.text_type(exception)
             entry_to_import.setErrorOutput(error_text)
             needs_notification_for_imported = True
         except OutdatedTranslationError as exception:
@@ -1005,15 +1006,15 @@ class POFile(SQLBase, POFileMixIn):
                 logger.info('Got an old version for %s' % self.title)
             template_mail = 'poimport-got-old-version.txt'
             import_rejected = True
-            error_text = str(exception)
+            error_text = six.text_type(exception)
             entry_to_import.setErrorOutput(
-                "Outdated translation.  " + error_text)
+                u"Outdated translation.  " + error_text)
         except TooManyPluralFormsError:
             if logger:
                 logger.warning("Too many plural forms.")
             template_mail = 'poimport-too-many-plural-forms.txt'
             import_rejected = True
-            entry_to_import.setErrorOutput("Too many plural forms.")
+            entry_to_import.setErrorOutput(u"Too many plural forms.")
         else:
             # The import succeeded.  There may still be non-fatal errors
             # or warnings for individual messages (kept as a list in
@@ -1047,7 +1048,7 @@ class POFile(SQLBase, POFileMixIn):
             data = self._prepare_pomessage_error_message(errors, replacements)
             subject, template_mail, errorsdetails = data
             entry_to_import.setErrorOutput(
-                "Imported, but with errors:\n" + errorsdetails)
+                u"Imported, but with errors:\n" + errorsdetails)
         else:
             # The import was successful.
             template_mail = 'poimport-confirmation.txt'
diff --git a/lib/lp/translations/model/potemplate.py b/lib/lp/translations/model/potemplate.py
index 70dedd2..9a622a0 100644
--- a/lib/lp/translations/model/potemplate.py
+++ b/lib/lp/translations/model/potemplate.py
@@ -944,7 +944,7 @@ class POTemplate(SQLBase, RosettaStats):
                 template_mail = 'poimport-syntax-error.txt'
             entry_to_import.setStatus(RosettaImportStatus.FAILED,
                                       rosetta_experts)
-            error_text = str(exception)
+            error_text = six.text_type(exception)
             entry_to_import.setErrorOutput(error_text)
         else:
             error_text = None
diff --git a/lib/lp/translations/model/translationimportqueue.py b/lib/lp/translations/model/translationimportqueue.py
index 483a4c3..fa41afa 100644
--- a/lib/lp/translations/model/translationimportqueue.py
+++ b/lib/lp/translations/model/translationimportqueue.py
@@ -1,6 +1,8 @@
 # Copyright 2009-2017 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
+from __future__ import absolute_import, print_function, unicode_literals
+
 __metaclass__ = type
 __all__ = [
     'collect_import_info',
@@ -20,20 +22,18 @@ from textwrap import dedent
 import posixpath
 import pytz
 import six
-from sqlobject import (
-    BoolCol,
-    ForeignKey,
-    SQLObjectNotFound,
-    StringCol,
-    )
 from storm.expr import (
     And,
     Or,
     Select,
     )
 from storm.locals import (
+    Bool,
+    DateTime,
     Int,
     Reference,
+    Store,
+    Unicode,
     )
 from zope.component import (
     getUtility,
@@ -59,20 +59,18 @@ from lp.services.database.constants import (
     DEFAULT,
     UTC_NOW,
     )
-from lp.services.database.datetimecol import UtcDateTimeCol
-from lp.services.database.enumcol import EnumCol
+from lp.services.database.enumcol import DBEnum
 from lp.services.database.interfaces import (
     IMasterStore,
     ISlaveStore,
     IStore,
     )
 from lp.services.database.sqlbase import (
-    cursor,
     quote,
     quote_like,
-    SQLBase,
-    sqlvalues,
     )
+from lp.services.database.stormbase import StormBase
+from lp.services.database.stormexpr import IsFalse
 from lp.services.librarian.interfaces.client import ILibrarianClient
 from lp.services.worlddata.interfaces.language import ILanguageSet
 from lp.translations.enums import RosettaImportStatus
@@ -169,18 +167,20 @@ def compose_approval_conflict_notice(domain, templates_count, sample):
 
 
 @implementer(ITranslationImportQueueEntry)
-class TranslationImportQueueEntry(SQLBase):
-
-    _table = 'TranslationImportQueueEntry'
-
-    path = StringCol(dbName='path', notNull=True)
-    content = ForeignKey(foreignKey='LibraryFileAlias', dbName='content',
-        notNull=False)
-    importer = ForeignKey(
-        dbName='importer', foreignKey='Person',
-        storm_validator=validate_person,
-        notNull=True)
-    dateimported = UtcDateTimeCol(dbName='dateimported', notNull=True,
+class TranslationImportQueueEntry(StormBase):
+
+    __storm_table__ = 'TranslationImportQueueEntry'
+
+    id = Int(primary=True)
+
+    path = Unicode(name='path', allow_none=False)
+    content_id = Int(name='content', allow_none=True)
+    content = Reference(content_id, 'LibraryFileAlias.id')
+    importer_id = Int(
+        name='importer', validator=validate_person, allow_none=False)
+    importer = Reference(importer_id, 'Person.id')
+    dateimported = DateTime(
+        tzinfo=pytz.UTC, name='dateimported', allow_none=False,
         default=DEFAULT)
     sourcepackagename_id = Int(name='sourcepackagename', allow_none=True)
     sourcepackagename = Reference(
@@ -189,18 +189,35 @@ class TranslationImportQueueEntry(SQLBase):
     distroseries = Reference(distroseries_id, 'DistroSeries.id')
     productseries_id = Int(name='productseries', allow_none=True)
     productseries = Reference(productseries_id, 'ProductSeries.id')
-    by_maintainer = BoolCol(notNull=True)
-    pofile = ForeignKey(foreignKey='POFile', dbName='pofile',
-        notNull=False, default=None)
-    potemplate = ForeignKey(foreignKey='POTemplate',
-        dbName='potemplate', notNull=False, default=None)
-    format = EnumCol(dbName='format', schema=TranslationFileFormat,
-        default=TranslationFileFormat.PO, notNull=True)
-    status = EnumCol(dbName='status', notNull=True,
-        schema=RosettaImportStatus, default=RosettaImportStatus.NEEDS_REVIEW)
-    date_status_changed = UtcDateTimeCol(dbName='date_status_changed',
-        notNull=True, default=DEFAULT)
-    error_output = StringCol(notNull=False, default=None)
+    by_maintainer = Bool(allow_none=False)
+    pofile_id = Int(name='pofile', allow_none=True, default=None)
+    pofile = Reference(pofile_id, 'POFile.id')
+    potemplate_id = Int(name='potemplate', allow_none=True, default=None)
+    potemplate = Reference(potemplate_id, 'POTemplate.id')
+    format = DBEnum(
+        name='format', enum=TranslationFileFormat,
+        default=TranslationFileFormat.PO, allow_none=False)
+    status = DBEnum(
+        name='status', allow_none=False,
+        enum=RosettaImportStatus, default=RosettaImportStatus.NEEDS_REVIEW)
+    date_status_changed = DateTime(
+        tzinfo=pytz.UTC, name='date_status_changed',
+        allow_none=False, default=DEFAULT)
+    error_output = Unicode(allow_none=True, default=None)
+
+    def __init__(self, path, content, importer, sourcepackagename,
+                 distroseries, productseries, by_maintainer, potemplate,
+                 pofile, format):
+        self.path = path
+        self.content = content
+        self.importer = importer
+        self.sourcepackagename = sourcepackagename
+        self.distroseries = distroseries
+        self.productseries = productseries
+        self.by_maintainer = by_maintainer
+        self.potemplate = potemplate
+        self.pofile = pofile
+        self.format = format
 
     @property
     def sourcepackage(self):
@@ -588,11 +605,12 @@ class TranslationImportQueueEntry(SQLBase):
                 # No way to guess anything...
                 return None
 
-            existing_entry = TranslationImportQueueEntry.selectOneBy(
+            existing_entry = IStore(TranslationImportQueueEntry).find(
+                TranslationImportQueueEntry,
                 importer=self.importer, path=self.path, potemplate=potemplate,
                 distroseries=self.distroseries,
                 sourcepackagename=self.sourcepackagename,
-                productseries=self.productseries)
+                productseries=self.productseries).one()
             if existing_entry is not None:
                 warning = ("%s: can't approve entry %d ('%s') "
                            "because entry %d is in the way." % (
@@ -796,24 +814,27 @@ class TranslationImportQueueEntry(SQLBase):
         importer = getUtility(ITranslationImporter)
         path = os.path.dirname(self.path)
 
-        suffix_clauses = [
-            "path LIKE '%%' || %s" % quote_like(suffix)
-            for suffix in importer.template_suffixes]
-
         clauses = [
-            "path LIKE %s || '%%'" % quote_like(path),
-            "id <> %s" % quote(self.id),
-            "(%s)" % " OR ".join(suffix_clauses)]
+            TranslationImportQueueEntry.path.startswith(path),
+            TranslationImportQueueEntry.id != self.id,
+            Or(*(
+                TranslationImportQueueEntry.path.endswith(suffix)
+                for suffix in importer.template_suffixes))]
 
         if self.distroseries is not None:
-            clauses.append('distroseries = %s' % quote(self.distroseries))
+            clauses.append(
+                TranslationImportQueueEntry.distroseries == self.distroseries)
         if self.sourcepackagename is not None:
             clauses.append(
-                'sourcepackagename = %s' % quote(self.sourcepackagename))
+                TranslationImportQueueEntry.sourcepackagename ==
+                    self.sourcepackagename)
         if self.productseries is not None:
-            clauses.append("productseries = %s" % quote(self.productseries))
+            clauses.append(
+                TranslationImportQueueEntry.productseries ==
+                    self.productseries)
 
-        return TranslationImportQueueEntry.select(" AND ".join(clauses))
+        return IStore(TranslationImportQueueEntry).find(
+            TranslationImportQueueEntry, *clauses)
 
     def getElapsedTimeText(self):
         """See ITranslationImportQueue."""
@@ -926,13 +947,15 @@ class TranslationImportQueue:
 
     def countEntries(self):
         """See `ITranslationImportQueue`."""
-        return TranslationImportQueueEntry.select().count()
+        return IStore(TranslationImportQueueEntry).find(
+            TranslationImportQueueEntry).count()
 
     def _iterNeedsReview(self):
         """Iterate over all entries in the queue that need review."""
-        return iter(TranslationImportQueueEntry.selectBy(
+        return iter(IStore(TranslationImportQueueEntry).find(
+            TranslationImportQueueEntry,
             status=RosettaImportStatus.NEEDS_REVIEW,
-            orderBy=['dateimported']))
+            ).order_by(TranslationImportQueueEntry.dateimported))
 
     def _getMatchingEntry(self, path, importer, potemplate, pofile,
                            sourcepackagename, distroseries, productseries):
@@ -1077,6 +1100,7 @@ class TranslationImportQueue:
         except TranslationImportQueueConflictError:
             return None
 
+        store = IMasterStore(TranslationImportQueueEntry)
         if entry is None:
             # It's a new row.
             entry = TranslationImportQueueEntry(path=path, content=alias,
@@ -1084,6 +1108,7 @@ class TranslationImportQueue:
                 distroseries=distroseries, productseries=productseries,
                 by_maintainer=by_maintainer, potemplate=potemplate,
                 pofile=pofile, format=format)
+            store.add(entry)
         else:
             # It's an update.
             entry.setErrorOutput(None)
@@ -1113,8 +1138,8 @@ class TranslationImportQueue:
 
             entry.date_status_changed = UTC_NOW
             entry.format = format
-            entry.sync()
 
+        store.flush()
         return entry
 
     def _iterTarballFiles(self, tarball):
@@ -1204,88 +1229,96 @@ class TranslationImportQueue:
 
     def get(self, id):
         """See ITranslationImportQueue."""
-        try:
-            return TranslationImportQueueEntry.get(id)
-        except SQLObjectNotFound:
-            return None
+        return IStore(TranslationImportQueueEntry).get(
+            TranslationImportQueueEntry, id)
 
     def _getQueryByFiltering(self, target=None, status=None,
                              file_extensions=None):
         """See `ITranslationImportQueue.`"""
-        queries = ["TRUE"]
-        clause_tables = []
+        # Avoid circular imports.
+        from lp.registry.model.distroseries import DistroSeries
+        from lp.registry.model.productseries import ProductSeries
+
+        queries = [True]
         if target is not None:
             if IPerson.providedBy(target):
-                queries.append('importer = %s' % sqlvalues(target))
+                queries.append(TranslationImportQueueEntry.importer == target)
             elif IProduct.providedBy(target):
-                queries.append('productseries = ProductSeries.id')
-                queries.append(
-                    'ProductSeries.product = %s' % sqlvalues(target))
-                clause_tables.append('ProductSeries')
+                queries.extend([
+                    TranslationImportQueueEntry.productseries ==
+                        ProductSeries.id,
+                    ProductSeries.product == target,
+                    ])
             elif IProductSeries.providedBy(target):
-                queries.append('productseries = %s' % sqlvalues(target))
-            elif IDistribution.providedBy(target):
-                queries.append('distroseries = DistroSeries.id')
                 queries.append(
-                    'DistroSeries.distribution = %s' % sqlvalues(target))
-                clause_tables.append('DistroSeries')
+                    TranslationImportQueueEntry.productseries == target)
+            elif IDistribution.providedBy(target):
+                queries.extend([
+                    TranslationImportQueueEntry.distroseries ==
+                        DistroSeries.id,
+                    DistroSeries.distribution == target,
+                    ])
             elif IDistroSeries.providedBy(target):
-                queries.append('distroseries = %s' % sqlvalues(target))
-            elif ISourcePackage.providedBy(target):
                 queries.append(
-                    'distroseries = %s' % sqlvalues(target.distroseries))
-                queries.append(
-                    'sourcepackagename = %s' % sqlvalues(
-                        target.sourcepackagename))
+                    TranslationImportQueueEntry.distroseries == target)
+            elif ISourcePackage.providedBy(target):
+                queries.extend([
+                    TranslationImportQueueEntry.distroseries ==
+                        target.distroseries,
+                    TranslationImportQueueEntry.sourcepackagename ==
+                        target.sourcepackagename,
+                    ])
             elif target == SpecialTranslationImportTargetFilter.PRODUCT:
-                queries.append('productseries IS NOT NULL')
+                queries.append(
+                    TranslationImportQueueEntry.productseries != None)
             elif target == SpecialTranslationImportTargetFilter.DISTRIBUTION:
-                queries.append('distroseries IS NOT NULL')
+                queries.append(
+                    TranslationImportQueueEntry.distroseries != None)
             else:
                 raise AssertionError(
                     'Target argument must be one of IPerson, IProduct,'
                     ' IProductSeries, IDistribution, IDistroSeries or'
                     ' ISourcePackage')
         if status is not None:
-            queries.append(
-                'TranslationImportQueueEntry.status = %s' % sqlvalues(status))
+            queries.append(TranslationImportQueueEntry.status == status)
         if file_extensions:
-            extension_clauses = [
-                "path LIKE '%%' || %s" % quote_like(extension)
-                for extension in file_extensions]
-            queries.append("(%s)" % " OR ".join(extension_clauses))
+            queries.append(Or(*(
+                TranslationImportQueueEntry.path.endswith(extension)
+                for extension in file_extensions)))
 
-        return queries, clause_tables
+        return queries
 
     def getAllEntries(self, target=None, import_status=None,
                       file_extensions=None):
         """See ITranslationImportQueue."""
-        queries, clause_tables = self._getQueryByFiltering(
+        queries = self._getQueryByFiltering(
             target, import_status, file_extensions)
-        return TranslationImportQueueEntry.select(
-            " AND ".join(queries), clauseTables=clause_tables,
-            orderBy=['status', 'dateimported', 'id'])
+        return IStore(TranslationImportQueueEntry).find(
+            TranslationImportQueueEntry, *queries).order_by(
+                'status', 'dateimported', 'id')
 
     def getFirstEntryToImport(self, target=None):
         """See ITranslationImportQueue."""
+        # Avoid circular import.
+        from lp.registry.model.distroseries import DistroSeries
+
         # Prepare the query to get only APPROVED entries.
-        queries, clause_tables = self._getQueryByFiltering(
+        queries = self._getQueryByFiltering(
             target, status=RosettaImportStatus.APPROVED)
 
         if (IDistribution.providedBy(target) or
-            IDistroSeries.providedBy(target) or
-            ISourcePackage.providedBy(target)):
-            # If the Distribution series has actived the option to defer
+                IDistroSeries.providedBy(target) or
+                ISourcePackage.providedBy(target)):
+            # If the Distribution series has activated the option to defer
             # translation imports, we ignore those entries.
-            if 'DistroSeries' not in clause_tables:
-                clause_tables.append('DistroSeries')
-                queries.append('distroseries = DistroSeries.id')
-
-            queries.append('DistroSeries.defer_translation_imports IS FALSE')
+            queries.extend([
+                TranslationImportQueueEntry.distroseries == DistroSeries.id,
+                IsFalse(DistroSeries.defer_translation_imports),
+                ])
 
-        return TranslationImportQueueEntry.selectFirst(
-            " AND ".join(queries), clauseTables=clause_tables,
-            orderBy=['dateimported'])
+        return IStore(TranslationImportQueueEntry).find(
+            TranslationImportQueueEntry,
+            *queries).order_by('dateimported').first()
 
     def getRequestTargets(self, user, status=None):
         """See `ITranslationImportQueue`."""
@@ -1487,8 +1520,7 @@ class TranslationImportQueue:
         """
         # XXX JeroenVermeulen 2009-09-18 bug=271938: Stormify this once
         # the Storm remove() syntax starts working properly for joins.
-        cur = cursor()
-        cur.execute("""
+        result = store.execute("""
             DELETE FROM TranslationImportQueueEntry AS Entry
             USING ProductSeries, Product
             WHERE
@@ -1496,7 +1528,7 @@ class TranslationImportQueue:
                 Product.id = ProductSeries.product AND
                 Product.active IS FALSE
             """)
-        return cur.rowcount
+        return result.rowcount
 
     def _cleanUpObsoleteDistroEntries(self, store):
         """Delete some queue entries for obsolete `DistroSeries`.
@@ -1506,8 +1538,7 @@ class TranslationImportQueue:
         """
         # XXX JeroenVermeulen 2009-09-18 bug=271938,432484: Stormify
         # this once Storm's remove() supports joins and slices.
-        cur = cursor()
-        cur.execute("""
+        result = store.execute("""
             DELETE FROM TranslationImportQueueEntry
             WHERE id IN (
                 SELECT Entry.id
@@ -1519,7 +1550,7 @@ class TranslationImportQueue:
                 WHERE DistroSeries.releasestatus = %s
                 LIMIT 100)
             """ % quote(SeriesStatus.OBSOLETE))
-        return cur.rowcount
+        return result.rowcount
 
     def cleanUpQueue(self):
         """See `ITranslationImportQueue`."""
@@ -1532,4 +1563,4 @@ class TranslationImportQueue:
 
     def remove(self, entry):
         """See ITranslationImportQueue."""
-        TranslationImportQueueEntry.delete(entry.id)
+        IMasterStore(TranslationImportQueueEntry).remove(entry)
diff --git a/lib/lp/translations/scripts/tests/test_translations_approval.py b/lib/lp/translations/scripts/tests/test_translations_approval.py
index fd4bd41..9a53f96 100644
--- a/lib/lp/translations/scripts/tests/test_translations_approval.py
+++ b/lib/lp/translations/scripts/tests/test_translations_approval.py
@@ -1,6 +1,8 @@
 # Copyright 2011 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
+from __future__ import absolute_import, print_function, unicode_literals
+
 import logging
 
 import transaction
diff --git a/lib/lp/translations/scripts/tests/test_translations_import.py b/lib/lp/translations/scripts/tests/test_translations_import.py
index bbb94cc..2fe67ae 100644
--- a/lib/lp/translations/scripts/tests/test_translations_import.py
+++ b/lib/lp/translations/scripts/tests/test_translations_import.py
@@ -1,6 +1,8 @@
 # Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
+from __future__ import absolute_import, print_function, unicode_literals
+
 import logging
 import re
 
@@ -47,7 +49,7 @@ class TestTranslationsImport(TestCaseWithFactory):
         """Produce a queue entry."""
         uploader = kwargs.pop('uploader', self.owner)
         return self.queue.addOrUpdateEntry(
-            path, '# Nothing here', False, uploader, **kwargs)
+            path, b'# Nothing here', False, uploader, **kwargs)
 
     def _makeApprovedEntry(self, uploader):
         """Produce an approved queue entry."""
diff --git a/lib/lp/translations/tests/test_autoapproval.py b/lib/lp/translations/tests/test_autoapproval.py
index 03c60b3..e1005e0 100644
--- a/lib/lp/translations/tests/test_autoapproval.py
+++ b/lib/lp/translations/tests/test_autoapproval.py
@@ -8,6 +8,8 @@ Documentation-style tests go in there, ones that go systematically
 through the possibilities should go here.
 """
 
+from __future__ import absolute_import, print_function, unicode_literals
+
 from contextlib import contextmanager
 from datetime import (
     datetime,
@@ -16,6 +18,7 @@ from datetime import (
 
 from fixtures import FakeLogger
 from pytz import UTC
+from storm.locals import Store
 from zope.component import getUtility
 from zope.security.proxy import removeSecurityProxy
 
@@ -189,7 +192,7 @@ class TestGuessPOFileCustomLanguageCode(TestCaseWithFactory,
     def _makeQueueEntry(self, language_code):
         """Create translation import queue entry."""
         return self.queue.addOrUpdateEntry(
-            "%s.po" % language_code, 'contents', True, self.product.owner,
+            "%s.po" % language_code, b'contents', True, self.product.owner,
             productseries=self.series)
 
     def _setCustomLanguageCode(self, language_code, target_language_code):
@@ -606,13 +609,13 @@ class TestTemplateGuess(TestCaseWithFactory, GardenerDbUserMixin):
         template.path = 'program/program.pot'
         self.producttemplate2.path = 'errors/errors.pot'
         entry1 = queue.addOrUpdateEntry(
-            'program/nl.po', 'contents', False, template.owner,
+            'program/nl.po', b'contents', False, template.owner,
             productseries=template.productseries)
 
         # The clashing entry goes through approval unsuccessfully, but
         # without causing breakage.
         queue.addOrUpdateEntry(
-            'program/nl.po', 'other contents', False, template.owner,
+            'program/nl.po', b'other contents', False, template.owner,
             productseries=template.productseries, potemplate=template)
 
         self.becomeTheGardener()
@@ -626,7 +629,7 @@ class TestTemplateGuess(TestCaseWithFactory, GardenerDbUserMixin):
         template.iscurrent = False
         queue = getUtility(ITranslationImportQueue)
         entry = queue.addOrUpdateEntry(
-            pofile.path, 'contents', False, self.factory.makePerson(),
+            pofile.path, b'contents', False, self.factory.makePerson(),
             productseries=template.productseries)
 
         self.assertEqual(None, entry.getGuessedPOFile())
@@ -645,7 +648,7 @@ class TestTemplateGuess(TestCaseWithFactory, GardenerDbUserMixin):
 
         queue = getUtility(ITranslationImportQueue)
         entry = queue.addOrUpdateEntry(
-            current_pofile.path, 'contents', False, self.factory.makePerson(),
+            current_pofile.path, b'contents', False, self.factory.makePerson(),
             productseries=series)
 
         self.assertEqual(current_pofile, entry.getGuessedPOFile())
@@ -662,7 +665,7 @@ class TestTemplateGuess(TestCaseWithFactory, GardenerDbUserMixin):
 
         queue = TranslationImportQueue()
         entry = queue.addOrUpdateEntry(
-            'test.pot', 'contents', False, template.owner,
+            'test.pot', b'contents', False, template.owner,
             productseries=template.productseries)
 
         self.assertEqual(template, entry.guessed_potemplate)
@@ -675,7 +678,7 @@ class TestTemplateGuess(TestCaseWithFactory, GardenerDbUserMixin):
 
         queue = TranslationImportQueue()
         entry = queue.addOrUpdateEntry(
-            'other.pot', 'contents', False, template.owner,
+            'other.pot', b'contents', False, template.owner,
             productseries=template.productseries)
 
         self.assertEqual(None, entry.guessed_potemplate)
@@ -691,7 +694,7 @@ class TestTemplateGuess(TestCaseWithFactory, GardenerDbUserMixin):
 
         queue = TranslationImportQueue()
         entry = queue.addOrUpdateEntry(
-            'test.pot', 'contents', False, template.owner,
+            'test.pot', b'contents', False, template.owner,
             productseries=template.productseries)
 
         self.assertEqual(None, entry.guessed_potemplate)
@@ -709,7 +712,7 @@ class TestTemplateGuess(TestCaseWithFactory, GardenerDbUserMixin):
 
         queue = TranslationImportQueue()
         entry = queue.addOrUpdateEntry(
-            'test.pot', 'contents', False, template.owner,
+            'test.pot', b'contents', False, template.owner,
             productseries=template.productseries)
 
         self.assertEqual(template, entry.guessed_potemplate)
@@ -724,10 +727,10 @@ class TestTemplateGuess(TestCaseWithFactory, GardenerDbUserMixin):
         queue = TranslationImportQueue()
         template = self.factory.makePOTemplate()
         old_entry = queue.addOrUpdateEntry(
-            template.path, '# Content here', False, template.owner,
+            template.path, b'# Content here', False, template.owner,
             productseries=template.productseries)
         new_entry = queue.addOrUpdateEntry(
-            template.path, '# Content here', False, template.owner,
+            template.path, b'# Content here', False, template.owner,
             productseries=template.productseries, potemplate=template)
 
         # Before approval, the two entries differ in that the new one
@@ -785,7 +788,7 @@ class TestKdePOFileGuess(TestCaseWithFactory, GardenerDbUserMixin):
             translation_domain='kde4')
         self.pofile_nl = nl_template.newPOFile('nl')
 
-        self.pocontents = """
+        self.pocontents = b"""
             msgid "foo"
             msgstr ""
             """
@@ -836,7 +839,7 @@ class TestGetPOFileFromLanguage(TestCaseWithFactory, GardenerDbUserMixin):
         template.iscurrent = True
 
         entry = self.queue.addOrUpdateEntry(
-            'nl.po', '# ...', False, template.owner, productseries=trunk)
+            'nl.po', b'# ...', False, template.owner, productseries=trunk)
 
         self.becomeTheGardener()
         pofile = entry._get_pofile_from_language('nl', 'domain')
@@ -854,7 +857,7 @@ class TestGetPOFileFromLanguage(TestCaseWithFactory, GardenerDbUserMixin):
         template.iscurrent = False
 
         entry = self.queue.addOrUpdateEntry(
-            'nl.po', '# ...', False, template.owner, productseries=trunk)
+            'nl.po', b'# ...', False, template.owner, productseries=trunk)
 
         self.becomeTheGardener()
         pofile = entry._get_pofile_from_language('nl', 'domain')
@@ -873,7 +876,7 @@ class TestGetPOFileFromLanguage(TestCaseWithFactory, GardenerDbUserMixin):
         self.factory.makePOTMsgSet(template, "translator-credits")
 
         entry = self.queue.addOrUpdateEntry(
-            'nl.po', '# ...', False, template.owner, productseries=trunk)
+            'nl.po', b'# ...', False, template.owner, productseries=trunk)
 
         self.becomeTheGardener()
         pofile = entry._get_pofile_from_language('nl', 'domain')
@@ -896,7 +899,7 @@ class TestCleanup(TestCaseWithFactory, GardenerDbUserMixin):
             translations_usage=ServiceUsage.LAUNCHPAD)
         trunk = product.getSeries('trunk')
         entry = self.queue.addOrUpdateEntry(
-            path, '# contents', False, product.owner, productseries=trunk)
+            path, b'# contents', False, product.owner, productseries=trunk)
         if status is not None:
             entry.status = status
         return entry
@@ -906,7 +909,7 @@ class TestCleanup(TestCaseWithFactory, GardenerDbUserMixin):
         package = self.factory.makeSourcePackage()
         owner = package.distroseries.owner
         entry = self.queue.addOrUpdateEntry(
-            path, '# contents', False, owner,
+            path, b'# contents', False, owner,
             sourcepackagename=package.sourcepackagename,
             distroseries=package.distroseries)
         if status is not None:
@@ -917,7 +920,7 @@ class TestCleanup(TestCaseWithFactory, GardenerDbUserMixin):
         """Make an entry's timestamps older by a given interval."""
         entry.dateimported -= interval
         entry.date_status_changed -= interval
-        entry.syncUpdate()
+        Store.of(entry).flush()
 
     def _exists(self, entry_id):
         """Is the entry with the given id still on the queue?"""
@@ -932,7 +935,7 @@ class TestCleanup(TestCaseWithFactory, GardenerDbUserMixin):
                         getUtility(ILaunchpadCelebrities).rosetta_experts)
         if when is not None:
             entry.date_status_changed = when
-        entry.syncUpdate()
+        Store.of(entry).flush()
 
     def test_cleanUpObsoleteEntries_unaffected_statuses(self):
         # _cleanUpObsoleteEntries leaves entries in states without
@@ -1106,7 +1109,7 @@ class TestAutoApprovalNewPOFile(TestCaseWithFactory, GardenerDbUserMixin):
     def _makeQueueEntry(self, series):
         """Create translation import queue entry."""
         return self.queue.addOrUpdateEntry(
-            "%s.po" % self.language.code, 'contents', True,
+            "%s.po" % self.language.code, b'contents', True,
             self.product.owner, productseries=series)
 
     def test_getGuessedPOFile_creates_POFile(self):
diff --git a/lib/lp/translations/tests/test_translationbranchapprover.py b/lib/lp/translations/tests/test_translationbranchapprover.py
index 43e4b36..fb77425 100644
--- a/lib/lp/translations/tests/test_translationbranchapprover.py
+++ b/lib/lp/translations/tests/test_translationbranchapprover.py
@@ -3,6 +3,8 @@
 
 """Translation File Auto Approver tests."""
 
+from __future__ import absolute_import, print_function, unicode_literals
+
 __metaclass__ = type
 
 from zope.component import getUtility
@@ -36,7 +38,7 @@ class TestTranslationBranchApprover(TestCaseWithFactory):
     def _upload_file(self, upload_path):
         # Put a template or translation file in the import queue.
         return self.queue.addOrUpdateEntry(upload_path,
-            self.factory.getUniqueString(), True, self.series.owner,
+            self.factory.getUniqueBytes(), True, self.series.owner,
             productseries=self.series)
 
     def _createTemplate(self, path, domain, productseries=None):
@@ -58,7 +60,7 @@ class TestTranslationBranchApprover(TestCaseWithFactory):
 
     def test_new_template_approved(self):
         # The approver puts new entries in the Approved state.
-        template_path = self.factory.getUniqueString() + u'.pot'
+        template_path = self.factory.getUniqueUnicode() + u'.pot'
         entry = self._upload_file(template_path)
         self.assertEqual(RosettaImportStatus.NEEDS_REVIEW, entry.status)
         self._createApprover(template_path).approve(entry)
@@ -88,7 +90,7 @@ class TestTranslationBranchApprover(TestCaseWithFactory):
             'sourcepackagename': package.sourcepackagename,
         }
         entry = self.queue.addOrUpdateEntry(
-            'messages.pot', self.factory.getUniqueString(), True,
+            'messages.pot', self.factory.getUniqueBytes(), True,
             self.factory.makePerson(), **package_kwargs)
 
         TranslationBranchApprover(entry.path, **package_kwargs).approve(entry)
@@ -104,7 +106,7 @@ class TestTranslationBranchApprover(TestCaseWithFactory):
     def test_new_template_domain(self):
         # The approver gets the translation domain for the entry from the
         # file path if possible.
-        translation_domain = self.factory.getUniqueString()
+        translation_domain = self.factory.getUniqueUnicode()
         template_path = translation_domain + u'.pot'
         entry = self._upload_file(template_path)
         self._createApprover(template_path).approve(entry)
@@ -123,7 +125,7 @@ class TestTranslationBranchApprover(TestCaseWithFactory):
 
     def test_replace_existing_approved(self):
         # Template files that replace existing entries are approved.
-        translation_domain = self.factory.getUniqueString()
+        translation_domain = self.factory.getUniqueUnicode()
         template_path = translation_domain + u'.pot'
         self._createTemplate(template_path, translation_domain)
         entry = self._upload_file(template_path)
@@ -133,7 +135,7 @@ class TestTranslationBranchApprover(TestCaseWithFactory):
     def test_replace_existing_potemplate(self):
         # When replacing an existing template, the queue entry is linked
         # to that existing entry.
-        translation_domain = self.factory.getUniqueString()
+        translation_domain = self.factory.getUniqueUnicode()
         template_path = translation_domain + u'.pot'
         potemplate = self._createTemplate(template_path, translation_domain)
         entry = self._upload_file(template_path)
@@ -143,7 +145,7 @@ class TestTranslationBranchApprover(TestCaseWithFactory):
     def test_ignore_existing_inactive_potemplate(self):
         # When replacing an existing inactive template, the entry is not
         # approved and no template is created for it.
-        translation_domain = self.factory.getUniqueString()
+        translation_domain = self.factory.getUniqueUnicode()
         template_path = translation_domain + u'.pot'
         potemplate = self._createTemplate(template_path, translation_domain)
         potemplate.setActive(False)
@@ -155,10 +157,10 @@ class TestTranslationBranchApprover(TestCaseWithFactory):
     def test_replace_existing_any_path(self):
         # If just one template file is found in the tree and just one
         # POTemplate is in the database, the upload is always approved.
-        existing_domain = self.factory.getUniqueString()
+        existing_domain = self.factory.getUniqueUnicode()
         existing_path = existing_domain + u'.pot'
         potemplate = self._createTemplate(existing_path, existing_domain)
-        template_path = self.factory.getUniqueString() + u'.pot'
+        template_path = self.factory.getUniqueUnicode() + u'.pot'
         entry = self._upload_file(template_path)
         self._createApprover(template_path).approve(entry)
         self.assertEqual(RosettaImportStatus.APPROVED, entry.status)
@@ -168,7 +170,7 @@ class TestTranslationBranchApprover(TestCaseWithFactory):
         # If an upload file has a generic path that does not yield a
         # translation domain, it is still approved if an entry with the
         # same file name exists.
-        translation_domain = self.factory.getUniqueString()
+        translation_domain = self.factory.getUniqueUnicode()
         generic_path = u'po/messages.pot'
         self._createTemplate(generic_path, translation_domain)
         entry = self._upload_file(generic_path)
@@ -191,7 +193,7 @@ class TestTranslationBranchApprover(TestCaseWithFactory):
         template = self.factory.makePOTemplate(**package_kwargs)
         original_domain = template.translation_domain
         entry = self.queue.addOrUpdateEntry(
-            generic_path, self.factory.getUniqueString(), True,
+            generic_path, self.factory.getUniqueBytes(), True,
             template.owner, potemplate=template, **package_kwargs)
 
         approver = TranslationBranchApprover(generic_path, **package_kwargs)
@@ -201,10 +203,10 @@ class TestTranslationBranchApprover(TestCaseWithFactory):
     def test_add_template(self):
         # When adding a template to an existing one it is approved if the
         # approver is told about both template files in the tree.
-        existing_domain = self.factory.getUniqueString()
+        existing_domain = self.factory.getUniqueUnicode()
         existing_path = u"%s/%s.pot" % (existing_domain, existing_domain)
         self._createTemplate(existing_path, existing_domain)
-        new_domain = self.factory.getUniqueString()
+        new_domain = self.factory.getUniqueUnicode()
         new_path = u"%s/%s.pot" % (new_domain, new_domain)
         entry = self._upload_file(new_path)
         self._createApprover((existing_path, new_path)).approve(entry)
@@ -214,8 +216,8 @@ class TestTranslationBranchApprover(TestCaseWithFactory):
     def test_upload_multiple_new_templates(self):
         # Multiple new templates can be added using the same
         # TranslationBranchApprover instance.
-        pot_path1 = self.factory.getUniqueString() + ".pot"
-        pot_path2 = self.factory.getUniqueString() + ".pot"
+        pot_path1 = self.factory.getUniqueUnicode() + ".pot"
+        pot_path2 = self.factory.getUniqueUnicode() + ".pot"
         entry1 = self._upload_file(pot_path1)
         entry2 = self._upload_file(pot_path2)
         approver = self._createApprover((pot_path1, pot_path2))
@@ -239,7 +241,7 @@ class TestTranslationBranchApprover(TestCaseWithFactory):
 
     def test_approve_only_if_needs_review(self):
         # If an entry is not in NEEDS_REVIEW state, it must not be approved.
-        pot_path = self.factory.getUniqueString() + ".pot"
+        pot_path = self.factory.getUniqueUnicode() + ".pot"
         entry = self._upload_file(pot_path)
         entry.potemplate = self.factory.makePOTemplate()
         not_approve_status = (
@@ -258,7 +260,7 @@ class TestTranslationBranchApprover(TestCaseWithFactory):
         # When the approver creates a new template, the new template
         # gets copies of any existing POFiles for templates that it will
         # share translations with.
-        domain = self.factory.getUniqueString()
+        domain = self.factory.getUniqueUnicode()
         pot_path = domain + ".pot"
         trunk = self.series.product.getSeries('trunk')
         trunk_template = self._createTemplate(
@@ -340,7 +342,7 @@ class TestBranchApproverPrivileges(TestCaseWithFactory):
         self.factory.makePackagingLink(
             productseries=productseries, **package_kwargs)
 
-        template_name = self.factory.getUniqueString()
+        template_name = self.factory.getUniqueUnicode()
         template_path = "%s.pot" % template_name
 
         self.factory.makePOFile(
diff --git a/lib/lp/translations/tests/test_translationbuildapprover.py b/lib/lp/translations/tests/test_translationbuildapprover.py
index c2a1d83..e38b2c2 100644
--- a/lib/lp/translations/tests/test_translationbuildapprover.py
+++ b/lib/lp/translations/tests/test_translationbuildapprover.py
@@ -3,6 +3,8 @@
 
 """Tests for the `TranslationBuildApprover`."""
 
+from __future__ import absolute_import, print_function, unicode_literals
+
 __metaclass__ = type
 
 from zope.component import getUtility
@@ -31,7 +33,7 @@ class TestTranslationBuildApprover(TestCaseWithFactory):
         """Create a list of queue entries and approve them."""
         return [
             approver.approve(self.queue.addOrUpdateEntry(
-                path, "#Dummy content.", False, self.uploader,
+                path, b"#Dummy content.", False, self.uploader,
                 productseries=series))
             for path in filenames]
 
diff --git a/lib/lp/translations/tests/test_translationimportqueue.py b/lib/lp/translations/tests/test_translationimportqueue.py
index c4ed52e..67a05e1 100644
--- a/lib/lp/translations/tests/test_translationimportqueue.py
+++ b/lib/lp/translations/tests/test_translationimportqueue.py
@@ -1,11 +1,14 @@
 # Copyright 2009-2018 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
+from __future__ import absolute_import, print_function, unicode_literals
+
 __metaclass__ = type
 
 from operator import attrgetter
 import os.path
 
+import six
 import transaction
 from zope.component import getUtility
 from zope.security.proxy import removeSecurityProxy
@@ -166,7 +169,7 @@ class TestCanSetStatusBase:
         # that are targeted to Ubuntu.
         self._setUpUbuntu()
         ubuntu_entry = self.queue.addOrUpdateEntry(
-            'demo.pot', '#demo', False, self.uploaderperson,
+            'demo.pot', b'#demo', False, self.uploaderperson,
             distroseries=self.factory.makeDistroSeries(self.ubuntu),
             sourcepackagename=self.factory.makeSourcePackageName(),
             potemplate=self.potemplate)
@@ -193,7 +196,7 @@ class TestCanSetStatusPOTemplate(TestCanSetStatusBase, TestCaseWithFactory):
         self.potemplate = self.factory.makePOTemplate(
             productseries=self.productseries)
         self.entry = self.queue.addOrUpdateEntry(
-            'demo.pot', '#demo', False, self.uploaderperson,
+            'demo.pot', b'#demo', False, self.uploaderperson,
             productseries=self.productseries, potemplate=self.potemplate)
 
 
@@ -209,7 +212,7 @@ class TestCanSetStatusPOFile(TestCanSetStatusBase, TestCaseWithFactory):
         self.pofile = self.factory.makePOFile(
             'eo', potemplate=self.potemplate)
         self.entry = self.queue.addOrUpdateEntry(
-            'demo.po', '#demo', False, self.uploaderperson,
+            'demo.po', b'#demo', False, self.uploaderperson,
             productseries=self.productseries, pofile=self.pofile)
 
 
@@ -279,8 +282,8 @@ class TestGetGuessedPOFile(TestCaseWithFactory):
         package, pot = self.createSourcePackageAndPOTemplate(
             source_name, template_name)
         queue_entry = self.queue.addOrUpdateEntry(
-            '%s.po' % template_path, template_name, True, self.uploaderperson,
-            distroseries=package.distroseries,
+            '%s.po' % template_path, six.ensure_binary(template_name), True,
+            self.uploaderperson, distroseries=package.distroseries,
             sourcepackagename=package.sourcepackagename)
         pofile = queue_entry.getGuessedPOFile()
         return (pot, pofile)
@@ -353,7 +356,7 @@ class TestProductOwnerEntryImporter(TestCaseWithFactory):
         # Changing the Product owner also updates the importer of the entry.
         with person_logged_in(self.old_owner):
             entry = self.import_queue.addOrUpdateEntry(
-                u'po/sr.po', 'foo', True, self.old_owner,
+                u'po/sr.po', b'foo', True, self.old_owner,
                 productseries=self.product.series[0])
             self.product.owner = self.new_owner
         self.assertEqual(self.new_owner, entry.importer)
@@ -364,11 +367,11 @@ class TestProductOwnerEntryImporter(TestCaseWithFactory):
         # cause an non-unique key for the entry.
         with person_logged_in(self.new_owner):
             self.import_queue.addOrUpdateEntry(
-                u'po/sr.po', 'foo', True, self.new_owner,
+                u'po/sr.po', b'foo', True, self.new_owner,
                 productseries=self.product.series[0])
         with person_logged_in(self.old_owner):
             old_entry = self.import_queue.addOrUpdateEntry(
-                u'po/sr.po', 'foo', True, self.old_owner,
+                u'po/sr.po', b'foo', True, self.old_owner,
                 productseries=self.product.series[0])
             self.product.owner = self.new_owner
         self.assertEqual(self.old_owner, old_entry.importer)
@@ -390,12 +393,12 @@ class TestTranslationImportQueue(TestCaseWithFactory):
 
         Returns a tuple (name, content).
         """
-        filename = self.factory.getUniqueString()
+        filename = self.factory.getUniqueUnicode()
         if extension is not None:
             filename = "%s.%s" % (filename, extension)
         if directory is not None:
             filename = os.path.join(directory, filename)
-        content = self.factory.getUniqueString()
+        content = self.factory.getUniqueBytes()
         return (filename, content)
 
     def _getQueuePaths(self):
@@ -469,7 +472,7 @@ class TestTranslationImportQueue(TestCaseWithFactory):
         # Repeated occurrence of the same approval conflict will not
         # result in repeated setting of error_output.
         series = self.factory.makeProductSeries()
-        domain = self.factory.getUniqueString()
+        domain = self.factory.getUniqueUnicode()
         templates = [
             self.factory.makePOTemplate(
                 productseries=series, translation_domain=domain)
@@ -509,7 +512,7 @@ class TestHelpers(TestCaseWithFactory):
         # The output from compose_approval_conflict_notice summarizes
         # the conflict: what translation domain is affected and how many
         # clashing templates are there?
-        domain = self.factory.getUniqueString()
+        domain = self.factory.getUniqueUnicode()
         num_templates = self.factory.getUniqueInteger()
 
         notice = compose_approval_conflict_notice(domain, num_templates, [])
@@ -526,9 +529,9 @@ class TestHelpers(TestCaseWithFactory):
             def __init__(self, displayname):
                 self.displayname = displayname
 
-        domain = self.factory.getUniqueString()
+        domain = self.factory.getUniqueUnicode()
         samples = [
-            FakePOTemplate(self.factory.getUniqueString())
+            FakePOTemplate(self.factory.getUniqueUnicode())
             for counter in range(3)]
         sorted_samples = sorted(samples, key=attrgetter('displayname'))
 
@@ -547,9 +550,9 @@ class TestHelpers(TestCaseWithFactory):
             def __init__(self, displayname):
                 self.displayname = displayname
 
-        domain = self.factory.getUniqueString()
+        domain = self.factory.getUniqueUnicode()
         samples = [
-            FakePOTemplate(self.factory.getUniqueString())
+            FakePOTemplate(self.factory.getUniqueUnicode())
             for counter in range(3)]
         samples.sort(key=attrgetter('displayname'))
 
diff --git a/lib/lp/translations/utilities/gettext_po_importer.py b/lib/lp/translations/utilities/gettext_po_importer.py
index 8b04120..79f99de 100644
--- a/lib/lp/translations/utilities/gettext_po_importer.py
+++ b/lib/lp/translations/utilities/gettext_po_importer.py
@@ -1,6 +1,8 @@
 # Copyright 2009-2010 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
+from __future__ import absolute_import, print_function, unicode_literals
+
 __metaclass__ = type
 
 __all__ = [
diff --git a/lib/lp/translations/utilities/tests/test_gettext_po_importer.py b/lib/lp/translations/utilities/tests/test_gettext_po_importer.py
index 0c57242..65ba8f6 100644
--- a/lib/lp/translations/utilities/tests/test_gettext_po_importer.py
+++ b/lib/lp/translations/utilities/tests/test_gettext_po_importer.py
@@ -3,6 +3,8 @@
 
 """Gettext PO importer tests."""
 
+from __future__ import absolute_import, print_function, unicode_literals
+
 __metaclass__ = type
 
 from io import BytesIO
@@ -27,7 +29,7 @@ from lp.translations.interfaces.translationimportqueue import (
 from lp.translations.utilities.gettext_po_importer import GettextPOImporter
 
 
-test_template = r'''
+test_template = br'''
 msgid ""
 msgstr ""
 "PO-Revision-Date: 2005-05-03 20:41+0100\n"
@@ -38,7 +40,7 @@ msgid "foo"
 msgstr ""
 '''
 
-test_translation_file = r'''
+test_translation_file = br'''
 msgid ""
 msgstr ""
 "PO-Revision-Date: 2005-05-03 20:41+0100\n"
diff --git a/lib/lp/translations/utilities/tests/test_kde_po_importer.py b/lib/lp/translations/utilities/tests/test_kde_po_importer.py
index 4aeb5eb..71407be 100644
--- a/lib/lp/translations/utilities/tests/test_kde_po_importer.py
+++ b/lib/lp/translations/utilities/tests/test_kde_po_importer.py
@@ -3,6 +3,8 @@
 
 """KDE PO importer tests."""
 
+from __future__ import absolute_import, print_function, unicode_literals
+
 __metaclass__ = type
 
 from io import BytesIO
@@ -31,7 +33,7 @@ from lp.translations.utilities.tests.test_gettext_po_importer import (
     )
 
 
-test_kde_template = r'''
+test_kde_template = br'''
 msgid ""
 msgstr ""
 "PO-Revision-Date: 2005-05-03 20:41+0100\n"
@@ -46,7 +48,7 @@ msgid "_: Context\nMessage"
 msgstr ""
 '''
 
-test_kde_translation_file = r'''
+test_kde_translation_file = br'''
 msgid ""
 msgstr ""
 "PO-Revision-Date: 2005-05-03 20:41+0100\n"
diff --git a/lib/lp/translations/utilities/tests/test_translation_importer.py b/lib/lp/translations/utilities/tests/test_translation_importer.py
index 2d47d01..106ca2d 100644
--- a/lib/lp/translations/utilities/tests/test_translation_importer.py
+++ b/lib/lp/translations/utilities/tests/test_translation_importer.py
@@ -3,6 +3,8 @@
 
 """Translation Importer tests."""
 
+from __future__ import absolute_import, print_function, unicode_literals
+
 __metaclass__ = type
 
 from io import BytesIO
@@ -233,7 +235,7 @@ class TranslationImporterTestCase(TestCaseWithFactory):
         existing_translation = self.factory.makeCurrentTranslationMessage(
             pofile=pofile, potmsgset=potmsgset1)
 
-        text = """
+        text = b"""
             msgid ""
             msgstr ""
             "MIME-Version: 1.0\\n"
diff --git a/lib/lp/translations/utilities/tests/test_xpi_po_exporter.py b/lib/lp/translations/utilities/tests/test_xpi_po_exporter.py
index 982fb97..78ab7cc 100644
--- a/lib/lp/translations/utilities/tests/test_xpi_po_exporter.py
+++ b/lib/lp/translations/utilities/tests/test_xpi_po_exporter.py
@@ -5,6 +5,8 @@
 # Copyright 2009-2017 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
+from __future__ import absolute_import, print_function, unicode_literals
+
 __metaclass__ = type
 
 from textwrap import dedent