Colin Watson has proposed merging ~cjwatson/launchpad:stormify-productrelease into launchpad:master.
Commit message: Convert ProductRelease and ProductReleaseFile to Storm Requested reviews: Launchpad code reviewers (launchpad-reviewers) For more details, see: https://code.launchpad.net/~cjwatson/launchpad/+git/launchpad/+merge/447211 -- Your team Launchpad code reviewers is requested to review the proposed merge of ~cjwatson/launchpad:stormify-productrelease into launchpad:master.
diff --git a/lib/lp/registry/model/productrelease.py b/lib/lp/registry/model/productrelease.py index aec8786..a73d29a 100644 --- a/lib/lp/registry/model/productrelease.py +++ b/lib/lp/registry/model/productrelease.py @@ -9,10 +9,15 @@ __all__ = [ ] import os +from datetime import timezone from io import BufferedIOBase, BytesIO +from operator import itemgetter -from storm.expr import And, Desc -from storm.store import EmptyResultSet +from storm.expr import And, Desc, Join, LeftJoin +from storm.info import ClassAlias +from storm.properties import DateTime, Int, Unicode +from storm.references import Reference, ReferenceSet +from storm.store import EmptyResultSet, Store from zope.component import getUtility from zope.interface import implementer @@ -30,16 +35,12 @@ from lp.registry.interfaces.productrelease import ( UpstreamFileType, ) 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, sqlvalues -from lp.services.database.sqlobject import ( - ForeignKey, - SQLMultipleJoin, - StringCol, -) +from lp.services.database.stormbase import StormBase from lp.services.librarian.interfaces import ILibraryFileAliasSet +from lp.services.librarian.model import LibraryFileAlias, LibraryFileContent from lp.services.propertycache import cachedproperty from lp.services.webapp.publisher import ( get_raw_form_value_from_current_request, @@ -47,33 +48,51 @@ from lp.services.webapp.publisher import ( @implementer(IProductRelease) -class ProductRelease(SQLBase): +class ProductRelease(StormBase): """A release of a product.""" - _table = "ProductRelease" - _defaultOrder = ["-datereleased"] + __storm_table__ = "ProductRelease" + __storm_order__ = ("-datereleased",) - datereleased = UtcDateTimeCol(notNull=True) - release_notes = StringCol(notNull=False, default=None) - changelog = StringCol(notNull=False, default=None) - datecreated = UtcDateTimeCol( - dbName="datecreated", notNull=True, default=UTC_NOW + id = Int(primary=True) + datereleased = DateTime(allow_none=False, tzinfo=timezone.utc) + release_notes = Unicode(allow_none=True, default=None) + changelog = Unicode(allow_none=True, default=None) + datecreated = DateTime( + name="datecreated", + allow_none=False, + default=UTC_NOW, + tzinfo=timezone.utc, ) - owner = ForeignKey( - dbName="owner", - foreignKey="Person", - storm_validator=validate_person, - notNull=True, - ) - milestone = ForeignKey(dbName="milestone", foreignKey="Milestone") - - _files = SQLMultipleJoin( - "ProductReleaseFile", - joinColumn="productrelease", - orderBy="-date_uploaded", - prejoins=["productrelease"], + owner_id = Int(name="owner", validator=validate_person, allow_none=False) + owner = Reference(owner_id, "Person.id") + milestone_id = Int(name="milestone", allow_none=False) + milestone = Reference(milestone_id, "Milestone.id") + + _files = ReferenceSet( + "id", + "ProductReleaseFile.productrelease_id", + order_by=Desc("ProductReleaseFile.date_uploaded"), ) + def __init__( + self, + datereleased, + owner, + milestone, + release_notes=None, + changelog=None, + ): + super().__init__() + self.owner = owner + self.milestone = milestone + self.datereleased = datereleased + self.release_notes = release_notes + self.changelog = changelog + + # This is cached so that + # lp.registry.model.product.get_precached_products can populate the + # cache from a bulk query. @cachedproperty def files(self): return self._files @@ -119,7 +138,7 @@ class ProductRelease(SQLBase): "You can't delete a product release which has files associated " "with it." ) - SQLBase.destroySelf(self) + Store.of(self).remove(self) def _getFileObjectAndSize(self, file_or_data): """Return an object and length for file_or_data. @@ -232,20 +251,21 @@ class ProductRelease(SQLBase): @implementer(IProductReleaseFile) -class ProductReleaseFile(SQLBase): +class ProductReleaseFile(StormBase): """A file of a product release.""" - _table = "ProductReleaseFile" + __storm_table__ = "ProductReleaseFile" - productrelease = ForeignKey( - dbName="productrelease", foreignKey="ProductRelease", notNull=True - ) + id = Int(primary=True) - libraryfile = ForeignKey( - dbName="libraryfile", foreignKey="LibraryFileAlias", notNull=True - ) + productrelease_id = Int(name="productrelease", allow_none=False) + productrelease = Reference(productrelease_id, "ProductRelease.id") - signature = ForeignKey(dbName="signature", foreignKey="LibraryFileAlias") + libraryfile_id = Int(name="libraryfile", allow_none=False) + libraryfile = Reference(libraryfile_id, "LibraryFileAlias.id") + + signature_id = Int(name="signature", allow_none=True) + signature = Reference(signature_id, "LibraryFileAlias.id") filetype = DBEnum( name="filetype", @@ -254,16 +274,37 @@ class ProductReleaseFile(SQLBase): default=UpstreamFileType.CODETARBALL, ) - description = StringCol(notNull=False, default=None) + description = Unicode(name="description", allow_none=True, default=None) + + uploader_id = Int( + name="uploader", validator=validate_public_person, allow_none=False + ) + uploader = Reference(uploader_id, "Person.id") - uploader = ForeignKey( - dbName="uploader", - foreignKey="Person", - storm_validator=validate_public_person, - notNull=True, + date_uploaded = DateTime( + allow_none=False, default=UTC_NOW, tzinfo=timezone.utc ) - date_uploaded = UtcDateTimeCol(notNull=True, default=UTC_NOW) + def __init__( + self, + productrelease, + libraryfile, + filetype, + uploader, + signature=None, + description=None, + ): + super().__init__() + self.productrelease = productrelease + self.libraryfile = libraryfile + self.filetype = filetype + self.uploader = uploader + self.signature = signature + self.description = description + + def destroySelf(self): + """See `IProductReleaseFile`.""" + Store.of(self).remove(self) @implementer(IProductReleaseSet) @@ -313,16 +354,42 @@ class ProductReleaseSet: releases = list(releases) if len(releases) == 0: return EmptyResultSet() - return ProductReleaseFile.select( - """ProductReleaseFile.productrelease IN %s""" - % (sqlvalues([release.id for release in releases])), - orderBy="-date_uploaded", - prejoins=[ - "libraryfile", - "libraryfile.content", - "productrelease", - "signature", - ], + SignatureAlias = ClassAlias(LibraryFileAlias) + return DecoratedResultSet( + IStore(ProductReleaseFile) + .using( + ProductReleaseFile, + Join( + LibraryFileAlias, + ProductReleaseFile.libraryfile_id == LibraryFileAlias.id, + ), + LeftJoin( + LibraryFileContent, + LibraryFileAlias.contentID == LibraryFileContent.id, + ), + Join( + ProductRelease, + ProductReleaseFile.productrelease_id == ProductRelease.id, + ), + LeftJoin( + SignatureAlias, + ProductReleaseFile.signature_id == SignatureAlias.id, + ), + ) + .find( + ( + ProductReleaseFile, + LibraryFileAlias, + LibraryFileContent, + ProductRelease, + SignatureAlias, + ), + ProductReleaseFile.productrelease_id.is_in( + [release.id for release in releases] + ), + ) + .order_by(Desc(ProductReleaseFile.date_uploaded)), + result_decorator=itemgetter(0), ) diff --git a/lib/lp/registry/model/productseries.py b/lib/lp/registry/model/productseries.py index 10d30ea..0bd1479 100644 --- a/lib/lp/registry/model/productseries.py +++ b/lib/lp/registry/model/productseries.py @@ -10,6 +10,7 @@ __all__ = [ ] import datetime +from operator import itemgetter from lazr.delegates import delegate_to from storm.expr import Max, Sum @@ -222,17 +223,13 @@ class ProductSeries( # The Milestone is cached too because most uses of a ProductRelease # need it. The decorated resultset returns just the ProductRelease. - def decorate(row): - product_release, milestone = row - return product_release - result = store.find( (ProductRelease, Milestone), Milestone.productseries == self, ProductRelease.milestone == Milestone.id, ) result = result.order_by(Desc("datereleased")) - return DecoratedResultSet(result, decorate) + return DecoratedResultSet(result, result_decorator=itemgetter(0)) @cachedproperty def _cached_releases(self): diff --git a/lib/lp/registry/scripts/productreleasefinder/finder.py b/lib/lp/registry/scripts/productreleasefinder/finder.py index 6126fb0..107a570 100644 --- a/lib/lp/registry/scripts/productreleasefinder/finder.py +++ b/lib/lp/registry/scripts/productreleasefinder/finder.py @@ -172,9 +172,9 @@ class ProductReleaseFinder: Product.name == product_name, Product.id == ProductSeries.productID, Milestone.productseriesID == ProductSeries.id, - ProductRelease.milestoneID == Milestone.id, - ProductReleaseFile.productreleaseID == ProductRelease.id, - LibraryFileAlias.id == ProductReleaseFile.libraryfileID, + ProductRelease.milestone_id == Milestone.id, + ProductReleaseFile.productrelease_id == ProductRelease.id, + LibraryFileAlias.id == ProductReleaseFile.libraryfile_id, ) file_names = set(found_names) return file_names diff --git a/lib/lp/registry/vocabularies.py b/lib/lp/registry/vocabularies.py index a9acabf..98333e0 100644 --- a/lib/lp/registry/vocabularies.py +++ b/lib/lp/registry/vocabularies.py @@ -1200,7 +1200,7 @@ class AllUserTeamsParticipationPlusSelfSimpleDisplayVocabulary( @implementer(IHugeVocabulary) -class ProductReleaseVocabulary(SQLObjectVocabularyBase): +class ProductReleaseVocabulary(StormVocabularyBase): """All `IProductRelease` objects vocabulary.""" displayname = "Select a Product Release" @@ -1210,8 +1210,12 @@ class ProductReleaseVocabulary(SQLObjectVocabularyBase): # Sorting by version won't give the expected results, because it's just a # text field. e.g. ["1.0", "2.0", "11.0"] would be sorted as ["1.0", # "11.0", "2.0"]. - _orderBy = [Product.q.name, ProductSeries.q.name, Milestone.q.name] - _clauseTables = ["Product", "ProductSeries"] + _order_by = [Product.name, ProductSeries.name, Milestone.name] + _clauses = [ + ProductRelease.milestone_id == Milestone.id, + Milestone.productseriesID == ProductSeries.id, + ProductSeries.productID == Product.id, + ] def toTerm(self, obj): """See `IVocabulary`.""" @@ -1240,14 +1244,17 @@ class ProductReleaseVocabulary(SQLObjectVocabularyBase): except ValueError: raise LookupError(token) - obj = ProductRelease.selectOne( - AND( - ProductRelease.q.milestoneID == Milestone.q.id, - Milestone.q.productseriesID == ProductSeries.q.id, - ProductSeries.q.productID == Product.q.id, - Product.q.name == productname, - ProductSeries.q.name == productseriesname, + obj = ( + IStore(ProductRelease) + .find( + ProductRelease, + ProductRelease.milestone_id == Milestone.id, + Milestone.productseriesID == ProductSeries.id, + ProductSeries.productID == Product.id, + Product.name == productname, + ProductSeries.name == productseriesname, ) + .one() ) try: return self.toTerm(obj) @@ -1259,22 +1266,22 @@ class ProductReleaseVocabulary(SQLObjectVocabularyBase): if not query: return self.emptySelectResults() - query = six.ensure_text(query).lower() - objs = self._table.select( - AND( - Milestone.q.id == ProductRelease.q.milestoneID, - ProductSeries.q.id == Milestone.q.productseriesID, - Product.q.id == ProductSeries.q.productID, - OR( - CONTAINSSTRING(Product.q.name, query), - CONTAINSSTRING(ProductSeries.q.name, query), + query = query.lower() + return ( + IStore(self._table) + .find( + self._table, + ProductRelease.milestone_id == Milestone.id, + Milestone.productseriesID == ProductSeries.id, + ProductSeries.productID == Product.id, + Or( + Product.name.contains_string(query), + ProductSeries.name.contains_string(query), ), - ), - orderBy=self._orderBy, + ) + .order_by(self._order_by) ) - return objs - @implementer(IHugeVocabulary) class ProductSeriesVocabulary(SQLObjectVocabularyBase): diff --git a/lib/lp/translations/doc/poimport-pofile-not-exported-from-rosetta.rst b/lib/lp/translations/doc/poimport-pofile-not-exported-from-rosetta.rst index a683b72..b0bf780 100644 --- a/lib/lp/translations/doc/poimport-pofile-not-exported-from-rosetta.rst +++ b/lib/lp/translations/doc/poimport-pofile-not-exported-from-rosetta.rst @@ -22,6 +22,7 @@ Here are some imports we need to get this test running. >>> from lp.app.interfaces.launchpad import ILaunchpadCelebrities >>> from lp.registry.interfaces.person import IPersonSet + >>> from lp.services.database.interfaces import IStore >>> from lp.translations.enums import RosettaImportStatus >>> from lp.translations.interfaces.translationimportqueue import ( ... ITranslationImportQueue, @@ -46,7 +47,7 @@ Here's the person who'll be doing the import. Now, is time to create the new potemplate >>> from lp.registry.model.productrelease import ProductRelease - >>> release = ProductRelease.get(3) + >>> release = IStore(ProductRelease).get(ProductRelease, 3) >>> print(release.milestone.productseries.product.name) firefox >>> series = release.milestone.productseries @@ -140,10 +141,9 @@ We should also be sure that we don't block any import that is coming from upstream. That kind of import is not blocked if they lack the 'X-Rosetta-Export-Date' header. -We need to fetch again some SQLObjects because we did a transaction -commit. +We need to fetch some rows again because we committed a transaction. - >>> release = ProductRelease.get(3) + >>> release = IStore(ProductRelease).get(ProductRelease, 3) >>> series = release.milestone.productseries >>> subset = POTemplateSubset(productseries=series) >>> potemplate = subset.getPOTemplateByName("firefox") diff --git a/lib/lp/translations/doc/poimport-pofile-old-po-imported.rst b/lib/lp/translations/doc/poimport-pofile-old-po-imported.rst index 4ee040a..9b077c8 100644 --- a/lib/lp/translations/doc/poimport-pofile-old-po-imported.rst +++ b/lib/lp/translations/doc/poimport-pofile-old-po-imported.rst @@ -20,6 +20,7 @@ Here are some imports we need to get this test running. >>> from lp.app.interfaces.launchpad import ILaunchpadCelebrities >>> from lp.registry.interfaces.person import IPersonSet + >>> from lp.services.database.interfaces import IStore >>> from lp.translations.interfaces.translationimportqueue import ( ... ITranslationImportQueue, ... ) @@ -44,7 +45,7 @@ Here's the person who'll be doing the import. Now it's time to create the new potemplate >>> from lp.registry.model.productrelease import ProductRelease - >>> release = ProductRelease.get(3) + >>> release = IStore(ProductRelease).get(ProductRelease, 3) >>> print(release.milestone.productseries.product.name) firefox >>> series = release.milestone.productseries diff --git a/lib/lp/translations/doc/poimport-pofile-syntax-error.rst b/lib/lp/translations/doc/poimport-pofile-syntax-error.rst index e2f7c91..30be238 100644 --- a/lib/lp/translations/doc/poimport-pofile-syntax-error.rst +++ b/lib/lp/translations/doc/poimport-pofile-syntax-error.rst @@ -10,6 +10,7 @@ Here are some imports we need to get this test running. >>> from lp.app.interfaces.launchpad import ILaunchpadCelebrities >>> from lp.registry.interfaces.person import IPersonSet + >>> from lp.services.database.interfaces import IStore >>> from lp.translations.interfaces.translationimportqueue import ( ... ITranslationImportQueue, ... ) @@ -38,7 +39,7 @@ Here's the person who'll be doing the import. Now, is time to create the new potemplate >>> from lp.registry.model.productrelease import ProductRelease - >>> release = ProductRelease.get(3) + >>> release = IStore(ProductRelease).get(ProductRelease, 3) >>> print(release.milestone.productseries.product.name) firefox diff --git a/lib/lp/translations/doc/poimport-potemplate-syntax-error.rst b/lib/lp/translations/doc/poimport-potemplate-syntax-error.rst index c14673d..a45fa79 100644 --- a/lib/lp/translations/doc/poimport-potemplate-syntax-error.rst +++ b/lib/lp/translations/doc/poimport-potemplate-syntax-error.rst @@ -10,6 +10,7 @@ Here are some imports we need to get this test running. >>> from lp.app.interfaces.launchpad import ILaunchpadCelebrities >>> from lp.registry.interfaces.person import IPersonSet + >>> from lp.services.database.interfaces import IStore >>> from lp.translations.interfaces.translationimportqueue import ( ... ITranslationImportQueue, ... ) @@ -37,7 +38,7 @@ Here's the person who'll be doing the import. Now, is time to create the new potemplate >>> from lp.registry.model.productrelease import ProductRelease - >>> release = ProductRelease.get(3) + >>> release = IStore(ProductRelease).get(ProductRelease, 3) >>> print(release.milestone.productseries.product.name) firefox >>> series = release.milestone.productseries
_______________________________________________ Mailing list: https://launchpad.net/~launchpad-reviewers Post to : launchpad-reviewers@lists.launchpad.net Unsubscribe : https://launchpad.net/~launchpad-reviewers More help : https://help.launchpad.net/ListHelp