← Back to team overview

launchpad-reviewers team mailing list archive

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

 

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

Commit message:
Convert ProductSeries to Storm

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/+git/launchpad/+merge/448908
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~cjwatson/launchpad:stormify-productseries into launchpad:master.
diff --git a/lib/lp/bugs/model/bugtask.py b/lib/lp/bugs/model/bugtask.py
index c8cc080..6752c85 100644
--- a/lib/lp/bugs/model/bugtask.py
+++ b/lib/lp/bugs/model/bugtask.py
@@ -864,11 +864,11 @@ class BugTask(StormBase):
                     break
         elif self.product:
             assert (
-                self.product.development_focusID is not None
+                self.product.development_focus_id is not None
             ), "A product should always have a development series."
-            devel_focusID = self.product.development_focusID
+            devel_focus_id = self.product.development_focus_id
             for bugtask in bugtasks:
-                if bugtask.productseries_id == devel_focusID:
+                if bugtask.productseries_id == devel_focus_id:
                     conjoined_primary = bugtask
                     break
 
@@ -1978,7 +1978,10 @@ class BugTaskSet:
                 ) AS subquery
             GROUP BY status
             """
-        query %= dict(series=quote(product_series), privacy=bug_privacy_filter)
+        query %= {
+            "series": quote(product_series.id),
+            "privacy": bug_privacy_filter,
+        }
         cur = cursor()
         cur.execute(query)
         return {
@@ -2114,7 +2117,7 @@ class BugTaskSet:
                 LeftJoin(
                     Product,
                     Product.id.is_in(
-                        (BugTaskFlat.product_id, ProductSeries.productID)
+                        (BugTaskFlat.product_id, ProductSeries.product_id)
                     ),
                 ),
             ),
@@ -2312,7 +2315,7 @@ class BugTaskSet:
             distro_series_ids.add(task.distroseries_id)
             product_ids.add(task.product_id)
             if task.productseries:
-                product_ids.add(task.productseries.productID)
+                product_ids.add(task.productseries.product_id)
             product_series_ids.add(task.productseries_id)
 
         distro_ids.discard(None)
diff --git a/lib/lp/bugs/model/bugtasksearch.py b/lib/lp/bugs/model/bugtasksearch.py
index f7a0fe1..222d404 100644
--- a/lib/lp/bugs/model/bugtasksearch.py
+++ b/lib/lp/bugs/model/bugtasksearch.py
@@ -1142,7 +1142,7 @@ def _build_exclude_conjoined_clause(milestone):
                     And(
                         ConjoinedPrimary.bug_id == BugTaskFlat.bug_id,
                         ConjoinedPrimary.productseries_id
-                        == Product.development_focusID,
+                        == Product.development_focus_id,
                         Not(
                             ConjoinedPrimary._status.is_in(
                                 BugTask._NON_CONJOINED_STATUSES
@@ -1154,7 +1154,7 @@ def _build_exclude_conjoined_clause(milestone):
             # join.right is the table name.
             join_tables = [(join.right, join) for join in joins]
         elif milestone.product is not None:
-            dev_focus_id = milestone.product.development_focusID
+            dev_focus_id = milestone.product.development_focus_id
             join = LeftJoin(
                 ConjoinedPrimary,
                 And(
diff --git a/lib/lp/bugs/vocabularies.py b/lib/lp/bugs/vocabularies.py
index ecf92ef..fd7fd08 100644
--- a/lib/lp/bugs/vocabularies.py
+++ b/lib/lp/bugs/vocabularies.py
@@ -55,7 +55,7 @@ from lp.services.webapp.interfaces import ILaunchBag
 from lp.services.webapp.vocabulary import (
     CountableIterator,
     IHugeVocabulary,
-    NamedSQLObjectVocabulary,
+    NamedStormVocabulary,
     StormVocabularyBase,
 )
 
@@ -255,7 +255,7 @@ def BugNominatableSeriesVocabulary(context=None):
         )
 
 
-class BugNominatableSeriesVocabularyBase(NamedSQLObjectVocabulary):
+class BugNominatableSeriesVocabularyBase(NamedStormVocabulary):
     """Base vocabulary class for series for which a bug can be nominated."""
 
     def __iter__(self):
@@ -265,6 +265,15 @@ class BugNominatableSeriesVocabularyBase(NamedSQLObjectVocabulary):
             if bug.canBeNominatedFor(series):
                 yield self.toTerm(series)
 
+    def __contains__(self, obj):
+        # NamedStormVocabulary implements this using a database query, but
+        # we need to go through __iter__ so that we filter the available
+        # series properly.
+        for term in self:
+            if term.value == obj:
+                return True
+        return False
+
     def toTerm(self, obj):
         return SimpleTerm(obj, obj.name, obj.name.capitalize())
 
@@ -292,7 +301,7 @@ class BugNominatableProductSeriesVocabulary(
     _table = ProductSeries
 
     def __init__(self, context, product):
-        BugNominatableSeriesVocabularyBase.__init__(self, context)
+        super().__init__(context)
         self.product = product
 
     def _getNominatableObjects(self):
@@ -310,7 +319,7 @@ class BugNominatableDistroSeriesVocabulary(BugNominatableSeriesVocabularyBase):
     _table = DistroSeries
 
     def __init__(self, context, distribution):
-        BugNominatableSeriesVocabularyBase.__init__(self, context)
+        super().__init__(context)
         self.distribution = distribution
 
     def _getNominatableObjects(self):
diff --git a/lib/lp/registry/interfaces/product.py b/lib/lp/registry/interfaces/product.py
index 429e26f..790f77d 100644
--- a/lib/lp/registry/interfaces/product.py
+++ b/lib/lp/registry/interfaces/product.py
@@ -894,7 +894,7 @@ class IProductView(
             ),
         )
     )
-    development_focusID = Attribute("The development focus ID.")
+    development_focus_id = Attribute("The development focus ID.")
 
     releases = exported(
         doNotSnapshot(
diff --git a/lib/lp/registry/interfaces/productseries.py b/lib/lp/registry/interfaces/productseries.py
index 391b8a1..83b1120 100644
--- a/lib/lp/registry/interfaces/productseries.py
+++ b/lib/lp/registry/interfaces/productseries.py
@@ -141,7 +141,7 @@ class IProductSeriesLimitedView(Interface):
         ),
         exported_as="project",
     )
-    productID = Attribute("The product ID.")
+    product_id = Attribute("The product ID.")
 
 
 class IProductSeriesView(
diff --git a/lib/lp/registry/model/person.py b/lib/lp/registry/model/person.py
index c0d94ca..1aa22bc 100644
--- a/lib/lp/registry/model/person.py
+++ b/lib/lp/registry/model/person.py
@@ -1746,7 +1746,7 @@ class Person(
             productseries = task.productseries
             distroseries = task.distroseries
             if productseries is not None and task.product is None:
-                dev_focus_id = productseries.product.development_focusID
+                dev_focus_id = productseries.product.development_focus_id
                 if (
                     productseries.id == dev_focus_id
                     and task.status not in BugTask._NON_CONJOINED_STATUSES
diff --git a/lib/lp/registry/model/product.py b/lib/lp/registry/model/product.py
index 87291df..aee58b0 100644
--- a/lib/lp/registry/model/product.py
+++ b/lib/lp/registry/model/product.py
@@ -31,7 +31,7 @@ from storm.expr import (
     Or,
     Select,
 )
-from storm.locals import Int, List, Reference, Store, Unicode
+from storm.locals import Int, List, Reference, ReferenceSet, Store, Unicode
 from zope.component import getUtility
 from zope.event import notify
 from zope.interface import implementer
@@ -151,7 +151,6 @@ from lp.services.database.sqlbase import SQLBase, sqlvalues
 from lp.services.database.sqlobject import (
     BoolCol,
     ForeignKey,
-    SQLMultipleJoin,
     SQLObjectNotFound,
     StringCol,
 )
@@ -344,12 +343,10 @@ class Product(
         enum=TranslationPermission,
         default=TranslationPermission.OPEN,
     )
-    translation_focus = ForeignKey(
-        dbName="translation_focus",
-        foreignKey="ProductSeries",
-        notNull=False,
-        default=None,
+    translation_focus_id = Int(
+        name="translation_focus", allow_none=True, default=None
     )
+    translation_focus = Reference(translation_focus_id, "ProductSeries.id")
     bugtracker_id = Int(name="bugtracker", allow_none=True, default=None)
     bugtracker = Reference(bugtracker_id, "BugTracker.id")
     official_answers = BoolCol(
@@ -675,12 +672,10 @@ class Product(
     # While the interface defines this field as required, we need to
     # allow it to be NULL so we can create new product records before
     # the corresponding series records.
-    development_focus = ForeignKey(
-        foreignKey="ProductSeries",
-        dbName="development_focus",
-        notNull=False,
-        default=None,
+    development_focus_id = Int(
+        name="development_focus", allow_none=True, default=None
     )
+    development_focus = Reference(development_focus_id, "ProductSeries.id")
     bug_reporting_guidelines = StringCol(default=None)
     bug_reported_acknowledgement = StringCol(default=None)
     enable_bugfiling_duplicate_search = BoolCol(notNull=True, default=True)
@@ -1020,8 +1015,8 @@ class Product(
         """Customize `search_params` for this product.."""
         search_params.setProduct(self)
 
-    _series = SQLMultipleJoin(
-        "ProductSeries", joinColumn="product", orderBy="name"
+    _series = ReferenceSet(
+        "id", "ProductSeries.product_id", order_by="ProductSeries.name"
     )
 
     @cachedproperty
@@ -1437,16 +1432,17 @@ class Product(
 
     def getSeries(self, name):
         """See `IProduct`."""
-        return ProductSeries.selectOneBy(product=self, name=name)
+        return (
+            IStore(ProductSeries)
+            .find(ProductSeries, product=self, name=name)
+            .one()
+        )
 
     def newSeries(
         self, owner, name, summary, branch=None, releasefileglob=None
     ):
-        # XXX: jamesh 2008-04-11
-        # Set the ID of the new ProductSeries to avoid flush order
-        # loops in ProductSet.createProduct()
         series = ProductSeries(
-            productID=self.id,
+            product=self,
             owner=owner,
             name=name,
             summary=summary,
@@ -1457,6 +1453,7 @@ class Product(
             # The user is a product driver, and should be the driver of this
             # series to make them the release manager.
             series.driver = owner
+        Store.of(series).flush()
         return series
 
     def getRelease(self, version):
@@ -1675,14 +1672,14 @@ def get_precached_products(
     if need_series:
         series_caches = {}
         for series in IStore(ProductSeries).find(
-            ProductSeries, ProductSeries.productID.is_in(product_ids)
+            ProductSeries, ProductSeries.product_id.is_in(product_ids)
         ):
             series_cache = get_property_cache(series)
             if need_releases and not hasattr(series_cache, "_cached_releases"):
                 series_cache._cached_releases = []
 
             series_caches[series.id] = series_cache
-            cache = caches[series.productID]
+            cache = caches[series.product_id]
             cache.series.append(series)
         if need_releases:
             release_caches = {}
@@ -1721,7 +1718,7 @@ def get_precached_products(
             ProjectGroup, products_by_id.values(), ["projectgroupID"]
         )
     bulk.load_related(
-        ProductSeries, products_by_id.values(), ["development_focusID"]
+        ProductSeries, products_by_id.values(), ["development_focus_id"]
     )
     if role_names is not None:
         person_ids = set()
@@ -1760,11 +1757,11 @@ def get_milestones_and_releases(products):
     store = IStore(Product)
     product_ids = [product.id for product in products]
     result = store.find(
-        (Milestone, ProductRelease, ProductSeries.productID),
+        (Milestone, ProductRelease, ProductSeries.product_id),
         And(
             ProductRelease.milestone == Milestone.id,
             Milestone.productseries == ProductSeries.id,
-            ProductSeries.productID.is_in(product_ids),
+            ProductSeries.product_id.is_in(product_ids),
         ),
     )
     return result.order_by(
@@ -1787,8 +1784,8 @@ def get_distro_sourcepackages(products):
     ]
     product_ids = [product.id for product in products]
     result = store.using(*origin).find(
-        (SourcePackageName, Distribution, ProductSeries.productID),
-        ProductSeries.productID.is_in(product_ids),
+        (SourcePackageName, Distribution, ProductSeries.product_id),
+        ProductSeries.product_id.is_in(product_ids),
     )
     result = result.order_by(SourcePackageName.name, Distribution.name)
     result.config(distinct=True)
@@ -2216,7 +2213,7 @@ class ProductSet:
             .find(
                 (Product, Person),
                 Product.active == True,
-                Product.id == ProductSeries.productID,
+                Product.id == ProductSeries.product_id,
                 POTemplate.productseries_id == ProductSeries.id,
                 Product.translations_usage == ServiceUsage.LAUNCHPAD,
                 Person.id == Product._ownerID,
diff --git a/lib/lp/registry/model/productseries.py b/lib/lp/registry/model/productseries.py
index 9f09770..e231439 100644
--- a/lib/lp/registry/model/productseries.py
+++ b/lib/lp/registry/model/productseries.py
@@ -14,7 +14,15 @@ from operator import itemgetter
 
 from lazr.delegates import delegate_to
 from storm.expr import Max, Sum
-from storm.locals import And, Desc, Int, Reference, ReferenceSet
+from storm.locals import (
+    And,
+    DateTime,
+    Desc,
+    Int,
+    Reference,
+    ReferenceSet,
+    Unicode,
+)
 from storm.store import Store
 from zope.component import getUtility
 from zope.interface import implementer
@@ -49,16 +57,10 @@ from lp.registry.model.milestone import HasMilestonesMixin, Milestone
 from lp.registry.model.productrelease import ProductRelease
 from lp.registry.model.series import SeriesMixin
 from lp.services.database.constants import UTC_NOW
-from lp.services.database.datetimecol import UtcDateTimeCol
 from lp.services.database.decoratedresultset import DecoratedResultSet
 from lp.services.database.enumcol import DBEnum
 from lp.services.database.interfaces import IStore
-from lp.services.database.sqlbase import SQLBase
-from lp.services.database.sqlobject import (
-    ForeignKey,
-    SQLObjectNotFound,
-    StringCol,
-)
+from lp.services.database.stormbase import StormBase
 from lp.services.propertycache import cachedproperty
 from lp.services.webapp.publisher import canonical_url
 from lp.services.webapp.sorting import sorted_dotted_numbers
@@ -95,7 +97,7 @@ def landmark_key(landmark):
 @delegate_to(ISpecificationTarget, context="product")
 @implementer(IBugSummaryDimension, IProductSeries, ISeriesBugTarget)
 class ProductSeries(
-    SQLBase,
+    StormBase,
     SeriesMixin,
     BugTargetBase,
     HasMilestonesMixin,
@@ -106,28 +108,25 @@ class ProductSeries(
 ):
     """A series of product releases."""
 
-    _table = "ProductSeries"
+    __storm_table__ = "ProductSeries"
 
-    product = ForeignKey(dbName="product", foreignKey="Product", notNull=True)
+    id = Int(primary=True)
+    product_id = Int(name="product", allow_none=False)
+    product = Reference(product_id, "Product.id")
     status = DBEnum(
         allow_none=False, enum=SeriesStatus, default=SeriesStatus.DEVELOPMENT
     )
-    name = StringCol(notNull=True)
-    datecreated = UtcDateTimeCol(notNull=True, default=UTC_NOW)
-    owner = ForeignKey(
-        dbName="owner",
-        foreignKey="Person",
-        storm_validator=validate_person,
-        notNull=True,
+    name = Unicode(allow_none=False)
+    datecreated = DateTime(
+        allow_none=False, default=UTC_NOW, tzinfo=datetime.timezone.utc
     )
+    owner_id = Int(name="owner", validator=validate_person, allow_none=False)
+    owner = Reference(owner_id, "Person.id")
 
-    driver = ForeignKey(
-        dbName="driver",
-        foreignKey="Person",
-        storm_validator=validate_person,
-        notNull=False,
-        default=None,
+    driver_id = Int(
+        name="driver", validator=validate_person, allow_none=True, default=None
     )
+    driver = Reference(driver_id, "Person.id")
     branch_id = Int(name="branch", default=None)
     branch = Reference(branch_id, "Branch.id")
 
@@ -156,13 +155,24 @@ class ProductSeries(
     )
     translations_branch = Reference(translations_branch_id, "Branch.id")
     # where are the tarballs released from this branch placed?
-    releasefileglob = StringCol(default=None)
-    releaseverstyle = StringCol(default=None)
+    releasefileglob = Unicode(default=None)
+    releaseverstyle = Unicode(default=None)
 
     packagings = ReferenceSet(
         "id", "Packaging.productseries_id", order_by=Desc("Packaging.id")
     )
 
+    def __init__(
+        self, product, name, owner, summary, branch=None, releasefileglob=None
+    ):
+        super().__init__()
+        self.product = product
+        self.name = name
+        self.owner = owner
+        self.summary = summary
+        self.branch = branch
+        self.releasefileglob = releasefileglob
+
     @property
     def pillar(self):
         """See `IBugTarget`."""
@@ -623,13 +633,13 @@ class ProductSeries(
 
         If the series isn't found, the product task is better than others.
         """
-        seriesID = self.id
-        productID = self.productID
+        series_id = self.id
+        product_id = self.product_id
 
         def weight_function(bugtask):
-            if bugtask.productseries_id == seriesID:
+            if bugtask.productseries_id == series_id:
                 return OrderedBugTask(1, bugtask.id, bugtask)
-            elif bugtask.product_id == productID:
+            elif bugtask.product_id == product_id:
                 return OrderedBugTask(2, bugtask.id, bugtask)
             else:
                 return OrderedBugTask(3, bugtask.id, bugtask)
@@ -670,10 +680,10 @@ class ProductSeriesSet:
 
     def get(self, series_id, default=None):
         """See IProductSeriesSet."""
-        try:
-            return ProductSeries.get(series_id)
-        except SQLObjectNotFound:
+        series = IStore(ProductSeries).get(ProductSeries, series_id)
+        if series is None:
             return default
+        return series
 
     def findByTranslationsImportBranch(
         self, branch, force_translations_upload=False
diff --git a/lib/lp/registry/model/projectgroup.py b/lib/lp/registry/model/projectgroup.py
index dfc4609..47be66b 100644
--- a/lib/lp/registry/model/projectgroup.py
+++ b/lib/lp/registry/model/projectgroup.py
@@ -65,7 +65,6 @@ from lp.services.database.enumcol import DBEnum
 from lp.services.database.interfaces import IStore
 from lp.services.database.sqlbase import SQLBase, sqlvalues
 from lp.services.database.sqlobject import (
-    AND,
     BoolCol,
     ForeignKey,
     SQLObjectNotFound,
@@ -221,7 +220,7 @@ class ProjectGroup(
         store = Store.of(self)
         origin = [
             Product,
-            Join(ProductSeries, Product.id == ProductSeries.productID),
+            Join(ProductSeries, Product.id == ProductSeries.product_id),
             Join(POTemplate, ProductSeries.id == POTemplate.productseries_id),
         ]
         return (
@@ -533,13 +532,16 @@ class ProjectGroup(
 
     def getSeries(self, series_name):
         """See `IProjectGroup.`"""
-        has_series = ProductSeries.selectFirst(
-            AND(
-                ProductSeries.q.productID == Product.q.id,
-                ProductSeries.q.name == series_name,
-                Product.q.projectgroupID == self.id,
-            ),
-            orderBy="id",
+        has_series = (
+            IStore(ProductSeries)
+            .find(
+                ProductSeries,
+                ProductSeries.product_id == Product.id,
+                ProductSeries.name == series_name,
+                Product.projectgroup == self,
+            )
+            .order_by(ProductSeries.id)
+            .first()
         )
 
         if has_series is None:
diff --git a/lib/lp/registry/model/series.py b/lib/lp/registry/model/series.py
index c65dff3..0de9426 100644
--- a/lib/lp/registry/model/series.py
+++ b/lib/lp/registry/model/series.py
@@ -10,11 +10,11 @@ __all__ = [
 
 from operator import attrgetter
 
+from storm.locals import Unicode
 from zope.interface import implementer
 
 from lp.registry.interfaces.series import ISeriesMixin, SeriesStatus
 from lp.registry.model.hasdrivers import HasDriversMixin
-from lp.services.database.sqlobject import StringCol
 
 ACTIVE_STATUSES = [
     SeriesStatus.DEVELOPMENT,
@@ -28,7 +28,7 @@ ACTIVE_STATUSES = [
 class SeriesMixin(HasDriversMixin):
     """See `ISeriesMixin`."""
 
-    summary = StringCol(notNull=True)
+    summary = Unicode(allow_none=False)
 
     @property
     def active(self):
diff --git a/lib/lp/registry/scripts/productreleasefinder/finder.py b/lib/lp/registry/scripts/productreleasefinder/finder.py
index 2271655..802c80e 100644
--- a/lib/lp/registry/scripts/productreleasefinder/finder.py
+++ b/lib/lp/registry/scripts/productreleasefinder/finder.py
@@ -119,7 +119,7 @@ class ProductReleaseFinder:
                     ProductSeries.name,
                     ProductSeries.releasefileglob,
                 ),
-                Product.id == ProductSeries.productID,
+                Product.id == ProductSeries.product_id,
                 Product.active == True,
                 ProductSeries.status != SeriesStatus.OBSOLETE,
                 ProductSeries.releasefileglob != None,
@@ -170,7 +170,7 @@ class ProductReleaseFinder:
         found_names = IStore(Product).find(
             LibraryFileAlias.filename,
             Product.name == product_name,
-            Product.id == ProductSeries.productID,
+            Product.id == ProductSeries.product_id,
             Milestone.productseries_id == ProductSeries.id,
             ProductRelease.milestone_id == Milestone.id,
             ProductReleaseFile.productrelease_id == ProductRelease.id,
diff --git a/lib/lp/registry/tests/test_product.py b/lib/lp/registry/tests/test_product.py
index d356ed1..895ee15 100644
--- a/lib/lp/registry/tests/test_product.py
+++ b/lib/lp/registry/tests/test_product.py
@@ -997,7 +997,7 @@ class TestProduct(TestCaseWithFactory):
             "datecreated",
             "description",
             "development_focus",
-            "development_focusID",
+            "development_focus_id",
             "direct_answer_contacts",
             "distrosourcepackages",
             "downloadurl",
diff --git a/lib/lp/registry/tests/test_productseries.py b/lib/lp/registry/tests/test_productseries.py
index e222af3..1188c19 100644
--- a/lib/lp/registry/tests/test_productseries.py
+++ b/lib/lp/registry/tests/test_productseries.py
@@ -672,7 +672,7 @@ class ProductSeriesSecurityAdaperTestCase(TestCaseWithFactory):
             "name",
             "parent_subscription_target",
             "product",
-            "productID",
+            "product_id",
             "series",
         },
         "launchpad.View": {
diff --git a/lib/lp/registry/vocabularies.py b/lib/lp/registry/vocabularies.py
index 566e9f7..1c1c5f4 100644
--- a/lib/lp/registry/vocabularies.py
+++ b/lib/lp/registry/vocabularies.py
@@ -1213,7 +1213,7 @@ class ProductReleaseVocabulary(StormVocabularyBase):
     _clauses = [
         ProductRelease.milestone_id == Milestone.id,
         Milestone.productseries_id == ProductSeries.id,
-        ProductSeries.productID == Product.id,
+        ProductSeries.product_id == Product.id,
     ]
 
     def toTerm(self, obj):
@@ -1249,7 +1249,7 @@ class ProductReleaseVocabulary(StormVocabularyBase):
                 ProductRelease,
                 ProductRelease.milestone_id == Milestone.id,
                 Milestone.productseries_id == ProductSeries.id,
-                ProductSeries.productID == Product.id,
+                ProductSeries.product_id == Product.id,
                 Product.name == productname,
                 ProductSeries.name == productseriesname,
             )
@@ -1272,7 +1272,7 @@ class ProductReleaseVocabulary(StormVocabularyBase):
                 self._table,
                 ProductRelease.milestone_id == Milestone.id,
                 Milestone.productseries_id == ProductSeries.id,
-                ProductSeries.productID == Product.id,
+                ProductSeries.product_id == Product.id,
                 Or(
                     Product.name.contains_string(query),
                     ProductSeries.name.contains_string(query),
@@ -1283,14 +1283,14 @@ class ProductReleaseVocabulary(StormVocabularyBase):
 
 
 @implementer(IHugeVocabulary)
-class ProductSeriesVocabulary(SQLObjectVocabularyBase):
+class ProductSeriesVocabulary(StormVocabularyBase):
     """All `IProductSeries` objects vocabulary."""
 
     displayname = "Select a Release Series"
     step_title = "Search"
     _table = ProductSeries
     _order_by = [Product.name, ProductSeries.name]
-    _clauseTables = ["Product"]
+    _clauses = [ProductSeries.product == Product.id]
 
     def toTerm(self, obj):
         """See `IVocabulary`."""
@@ -1334,17 +1334,17 @@ class ProductSeriesVocabulary(SQLObjectVocabularyBase):
         if "/" in query:
             product_query, series_query = query.split("/", 1)
             substring_search = And(
-                CONTAINSSTRING(Product.name, product_query),
-                CONTAINSSTRING(ProductSeries.name, series_query),
+                Product.name.contains_string(product_query),
+                ProductSeries.name.contains_string(series_query),
             )
         else:
             substring_search = Or(
-                CONTAINSSTRING(Product.name, query),
-                CONTAINSSTRING(ProductSeries.name, query),
+                Product.name.contains_string(query),
+                ProductSeries.name.contains_string(query),
             )
         result = IStore(self._table).find(
             self._table,
-            Product.id == ProductSeries.productID,
+            Product.id == ProductSeries.product_id,
             substring_search,
             privacy_filter,
         )
@@ -1376,11 +1376,11 @@ class FilteredDistroSeriesVocabulary(SQLObjectVocabularyBase):
                 yield self.toTerm(series)
 
 
-class FilteredProductSeriesVocabulary(SQLObjectVocabularyBase):
+class FilteredProductSeriesVocabulary(StormVocabularyBase):
     """Describes ProductSeries of a particular product."""
 
     _table = ProductSeries
-    _orderBy = ["product", "name"]
+    _order_by = ["product", "name"]
 
     def toTerm(self, obj):
         """See `IVocabulary`."""
diff --git a/lib/lp/translations/browser/potemplate.py b/lib/lp/translations/browser/potemplate.py
index 12a684f..78d8a83 100644
--- a/lib/lp/translations/browser/potemplate.py
+++ b/lib/lp/translations/browser/potemplate.py
@@ -1036,7 +1036,7 @@ class BaseSeriesTemplatesView(LaunchpadView):
             .joinOuter(
                 Product,
                 And(
-                    Product.id == ProductSeries.productID,
+                    Product.id == ProductSeries.product_id,
                     Or(
                         Product.translations_usage == ServiceUsage.LAUNCHPAD,
                         Product.translations_usage == ServiceUsage.EXTERNAL,
diff --git a/lib/lp/translations/doc/potemplate.rst b/lib/lp/translations/doc/potemplate.rst
index 25b03a2..d9e4e26 100644
--- a/lib/lp/translations/doc/potemplate.rst
+++ b/lib/lp/translations/doc/potemplate.rst
@@ -90,7 +90,8 @@ This method gives us the IPOTemplate that belongs to this subset and its
 name is the given one.
 
     >>> from lp.registry.model.productseries import ProductSeries
-    >>> productseries = ProductSeries.get(3)
+    >>> from lp.services.database.interfaces import IStore
+    >>> productseries = IStore(ProductSeries).get(ProductSeries, 3)
     >>> potemplatesubset = potemplate_set.getSubset(
     ...     productseries=productseries
     ... )
@@ -137,7 +138,7 @@ To do this test, first we check the evolution product, it has two
 potemplates in the same path and thus, this method should not get any
 value.
 
-    >>> productseries = ProductSeries.get(3)
+    >>> productseries = IStore(ProductSeries).get(ProductSeries, 3)
     >>> potemplatesubset = potemplate_set.getSubset(
     ...     productseries=productseries
     ... )
@@ -153,7 +154,7 @@ value.
 
 Now, we move to the NetApplet product, we should detect it.
 
-    >>> productseries = ProductSeries.get(5)
+    >>> productseries = IStore(ProductSeries).get(ProductSeries, 5)
     >>> potemplatesubset = potemplate_set.getSubset(
     ...     productseries=productseries
     ... )
@@ -181,7 +182,6 @@ POTemplate
 POTemplate is an object with all strings that must be translated for a
 concrete context.
 
-    >>> from lp.services.database.interfaces import IStore
     >>> from lp.testing import verifyObject
     >>> from lp.translations.interfaces.potemplate import IPOTemplate
     >>> from lp.translations.model.potemplate import POTemplate
diff --git a/lib/lp/translations/doc/translationimportqueue.rst b/lib/lp/translations/doc/translationimportqueue.rst
index edf81ad..e8f0793 100644
--- a/lib/lp/translations/doc/translationimportqueue.rst
+++ b/lib/lp/translations/doc/translationimportqueue.rst
@@ -82,7 +82,7 @@ Login as a user without privileges.
 First, we are going to try to do the guess against the Evolution product. That
 means that we are going to use the ProductSeries.id = 3
 
-    >>> evolution_productseries = ProductSeries.get(3)
+    >>> evolution_productseries = IStore(ProductSeries).get(ProductSeries, 3)
 
 Attach the file to the product series, without associating it with any
 potemplate.
@@ -1022,7 +1022,7 @@ was added to the queue.
 
 We need to attach a new entry to play with:
 
-    >>> productseries = ProductSeries.get(1)
+    >>> productseries = IStore(ProductSeries).get(ProductSeries, 1)
     >>> entry = translationimportqueue.addOrUpdateEntry(
     ...     "foo/bar.po",
     ...     b"foo content",
diff --git a/lib/lp/translations/model/potemplate.py b/lib/lp/translations/model/potemplate.py
index 58d73cb..22fa91a 100644
--- a/lib/lp/translations/model/potemplate.py
+++ b/lib/lp/translations/model/potemplate.py
@@ -1531,7 +1531,7 @@ class POTemplateSet:
         from lp.registry.model.productseries import ProductSeries
 
         pses = load_related(ProductSeries, templates, ["productseries_id"])
-        load_related(Product, pses, ["productID"])
+        load_related(Product, pses, ["product_id"])
         load_related(SourcePackageName, templates, ["sourcepackagename_id"])
 
     def wipeSuggestivePOTemplatesCache(self):
@@ -1694,7 +1694,7 @@ class POTemplateSharingSubset:
                 DistroSeries.distributionID == Location.distribution_id,
             ),
             LeftJoin(
-                ProductSeries, ProductSeries.productID == Location.product_id
+                ProductSeries, ProductSeries.product_id == Location.product_id
             ),
             Join(
                 POTemplate,
diff --git a/lib/lp/translations/model/translationimportqueue.py b/lib/lp/translations/model/translationimportqueue.py
index 3565e27..af10e15 100644
--- a/lib/lp/translations/model/translationimportqueue.py
+++ b/lib/lp/translations/model/translationimportqueue.py
@@ -924,7 +924,7 @@ def list_product_request_targets(user, status_condition):
 
     products = IStore(Product).find(
         Product,
-        Product.id == ProductSeries.productID,
+        Product.id == ProductSeries.product_id,
         Product.active == True,
         ProductSeries.id.is_in(
             Select(
diff --git a/lib/lp/translations/model/translationsperson.py b/lib/lp/translations/model/translationsperson.py
index 96529b1..b8e0cc4 100644
--- a/lib/lp/translations/model/translationsperson.py
+++ b/lib/lp/translations/model/translationsperson.py
@@ -336,7 +336,7 @@ class TranslationsPerson:
                     Join(
                         Product,
                         And(
-                            Product.id == ProductSeries.productID,
+                            Product.id == ProductSeries.product_id,
                             Product.translations_usage
                             == ServiceUsage.LAUNCHPAD,
                             Product.active == True,
@@ -409,7 +409,7 @@ class TranslationsPerson:
         ProductJoin = LeftJoin(
             Product,
             And(
-                Product.id == ProductSeries.productID,
+                Product.id == ProductSeries.product_id,
                 Product.translations_usage == ServiceUsage.LAUNCHPAD,
                 Product.active == True,
             ),
diff --git a/lib/lp/translations/scripts/migrate_current_flag.py b/lib/lp/translations/scripts/migrate_current_flag.py
index ced0c6a..8a6f430 100644
--- a/lib/lp/translations/scripts/migrate_current_flag.py
+++ b/lib/lp/translations/scripts/migrate_current_flag.py
@@ -134,7 +134,7 @@ class MigrateCurrentFlagProcess:
             self.store.find(
                 Product,
                 POTemplate.productseries_id == ProductSeries.id,
-                ProductSeries.productID == Product.id,
+                ProductSeries.product_id == Product.id,
             )
             .group_by(Product)
             .having(Count(POTemplate.id) > 0)
@@ -152,7 +152,7 @@ class MigrateCurrentFlagProcess:
             ),
             TranslationTemplateItem.potemplate_id == POTemplate.id,
             POTemplate.productseries_id == ProductSeries.id,
-            ProductSeries.productID == product.id,
+            ProductSeries.product_id == product.id,
         ).config(distinct=True)
 
     def run(self):
diff --git a/lib/lp/translations/scripts/scrub_pofiletranslator.py b/lib/lp/translations/scripts/scrub_pofiletranslator.py
index 5536710..99048e0 100644
--- a/lib/lp/translations/scripts/scrub_pofiletranslator.py
+++ b/lib/lp/translations/scripts/scrub_pofiletranslator.py
@@ -256,7 +256,7 @@ def preload_work_items(work_items):
     productseries = load_related(
         ProductSeries, templates, ["productseries_id"]
     )
-    load_related(Product, productseries, ["productID"])
+    load_related(Product, productseries, ["product_id"])
     return {pofile.id: pofile for pofile in pofiles}