Merge lp:~lifeless/launchpad/bug-727020 into lp:launchpad

Proposed by Robert Collins
Status: Merged
Approved by: Robert Collins
Approved revision: no longer in the source branch.
Merged at revision: 12643
Proposed branch: lp:~lifeless/launchpad/bug-727020
Merge into: lp:launchpad
Diff against target: 234 lines (+79/-58)
4 files modified
lib/lp/bugs/browser/bugalsoaffects.py (+4/-4)
lib/lp/code/model/branchcollection.py (+2/-4)
lib/lp/registry/interfaces/product.py (+12/-6)
lib/lp/registry/model/product.py (+61/-44)
To merge this branch: bzr merge lp:~lifeless/launchpad/bug-727020
Reviewer Review Type Date Requested Status
Steve Kowalik (community) Approve
Review via email: mp+54290@code.launchpad.net

Commit message

[r=stevenk][bug=727020] Eager load most fields for /api/1.0//products. OfficialBugTags are a bit hard for now.

Description of the change

More eager loading. How boring.

To post a comment you must log in.
Revision history for this message
Steve Kowalik (stevenk) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/bugs/browser/bugalsoaffects.py'
2--- lib/lp/bugs/browser/bugalsoaffects.py 2011-03-09 22:07:20 +0000
3+++ lib/lp/bugs/browser/bugalsoaffects.py 2011-03-22 08:11:06 +0000
4@@ -767,10 +767,10 @@
5 # Use a local import as we don't want removeSecurityProxy used
6 # anywhere else.
7 from zope.security.proxy import removeSecurityProxy
8- name_matches = getUtility(IProductSet).search(
9- self.request.form.get('field.name'))
10- products = bugtracker.products.intersect(
11- removeSecurityProxy(name_matches))
12+ name_matches = removeSecurityProxy(
13+ getUtility(IProductSet).search_sqlobject(
14+ self.request.form.get('field.name')))
15+ products = bugtracker.products.intersect(name_matches)
16 self.existing_products = list(
17 products[:self.MAX_PRODUCTS_TO_DISPLAY])
18 else:
19
20=== modified file 'lib/lp/code/model/branchcollection.py'
21--- lib/lp/code/model/branchcollection.py 2011-03-21 03:50:10 +0000
22+++ lib/lp/code/model/branchcollection.py 2011-03-22 08:11:06 +0000
23@@ -183,6 +183,8 @@
24 cache._associatedProductSeries = []
25 if not safe_hasattr(cache, '_associatedSuiteSourcePackages'):
26 cache._associatedSuiteSourcePackages = []
27+ if not safe_hasattr(cache, 'code_import'):
28+ cache.code_import = None
29 # associatedProductSeries
30 # Imported here to avoid circular import.
31 from lp.registry.model.productseries import ProductSeries
32@@ -206,10 +208,6 @@
33 # need for validity etc in the /branches API call.
34 load_related(Person, rows,
35 ['ownerID', 'registrantID', 'reviewerID'])
36- # Cache all branches as having no code imports to prevent fruitless
37- # lookups on the ones we don't find.
38- for cache in caches.values():
39- cache.code_import = None
40 for code_import in IStore(CodeImport).find(
41 CodeImport, CodeImport.branchID.is_in(branch_ids)):
42 cache = caches[code_import.branchID]
43
44=== modified file 'lib/lp/registry/interfaces/product.py'
45--- lib/lp/registry/interfaces/product.py 2011-02-24 15:30:54 +0000
46+++ lib/lp/registry/interfaces/product.py 2011-03-22 08:11:06 +0000
47@@ -938,14 +938,20 @@
48 @operation_parameters(text=TextLine(title=_("Search text")))
49 @operation_returns_collection_of(IProduct)
50 @export_read_operation()
51- def search(text=None, soyuz=None,
52- rosetta=None, malone=None,
53- bazaar=None):
54+ def search(text=None):
55 """Search through the Registry database for products that match the
56 query terms. text is a piece of text in the title / summary /
57- description fields of product. soyuz, bazaar, malone etc are
58- hints as to whether the search should be limited to products
59- that are active in those Launchpad applications."""
60+ description fields of product.
61+
62+ This call eager loads data appropriate for web API; caution may be
63+ needed for other callers.
64+ """
65+
66+ def search_sqlobject(text):
67+ """A compatible sqlobject search for bugalsoaffects.py.
68+
69+ DO NOT ADD USES.
70+ """
71
72 @operation_returns_collection_of(IProduct)
73 @call_with(quantity=None)
74
75=== modified file 'lib/lp/registry/model/product.py'
76--- lib/lp/registry/model/product.py 2011-03-03 16:49:09 +0000
77+++ lib/lp/registry/model/product.py 2011-03-22 08:11:06 +0000
78@@ -78,6 +78,7 @@
79 IStoreSelector,
80 MAIN_STORE,
81 )
82+from canonical.lazr.utils import safe_hasattr
83 from lp.answers.interfaces.faqtarget import IFAQTarget
84 from lp.answers.interfaces.questioncollection import (
85 QUESTION_STATUS_DEFAULT_SEARCH,
86@@ -165,6 +166,7 @@
87 from lp.registry.model.productseries import ProductSeries
88 from lp.registry.model.series import ACTIVE_STATUSES
89 from lp.registry.model.sourcepackagename import SourcePackageName
90+from lp.services.database import bulk
91 from lp.services.propertycache import (
92 cachedproperty,
93 get_property_cache,
94@@ -459,7 +461,6 @@
95 bug_reporting_guidelines = StringCol(default=None)
96 bug_reported_acknowledgement = StringCol(default=None)
97 enable_bugfiling_duplicate_search = BoolCol(notNull=True, default=True)
98- _cached_licenses = None
99
100 def _validate_active(self, attr, value):
101 # Validate deactivation.
102@@ -639,11 +640,6 @@
103 self.license_reviewed = False
104 self.license_approved = False
105
106- def __storm_invalidated__(self):
107- """Clear cached non-storm attributes when the transaction ends."""
108- super(Product, self).__storm_invalidated__()
109- self._cached_licenses = None
110-
111 def _get_answers_usage(self):
112 if self._answers_usage != ServiceUsage.UNKNOWN:
113 # If someone has set something with the enum, use it.
114@@ -704,14 +700,16 @@
115 _set_translations_usage,
116 doc="Indicates if the product uses the translations service.")
117
118+ @cachedproperty
119+ def _cached_licenses(self):
120+ """Get the licenses as a tuple."""
121+ product_licenses = ProductLicense.selectBy(
122+ product=self, orderBy='license')
123+ return tuple(
124+ product_license.license
125+ for product_license in product_licenses)
126+
127 def _getLicenses(self):
128- """Get the licenses as a tuple."""
129- if self._cached_licenses is None:
130- product_licenses = ProductLicense.selectBy(
131- product=self, orderBy='license')
132- self._cached_licenses = tuple(
133- product_license.license
134- for product_license in product_licenses)
135 return self._cached_licenses
136
137 def _setLicenses(self, licenses, reset_license_reviewed=True):
138@@ -746,7 +744,7 @@
139
140 for license in licenses.difference(old_licenses):
141 ProductLicense(product=self, license=license)
142- self._cached_licenses = tuple(sorted(licenses))
143+ get_property_cache(self)._cached_licenses = tuple(sorted(licenses))
144
145 licenses = property(_getLicenses, _setLicenses)
146
147@@ -1575,38 +1573,57 @@
148 Product.datecreated, Product.displayname)
149 return result
150
151- def search(self, text=None, soyuz=None,
152- rosetta=None, malone=None,
153- bazaar=None,
154- show_inactive=False):
155+ def search(self, text=None):
156 """See lp.registry.interfaces.product.IProductSet."""
157- # XXX: kiko 2006-03-22: The soyuz argument is unused.
158- clauseTables = set()
159- clauseTables.add('Product')
160- queries = []
161+ # Circular...
162+ from lp.registry.model.projectgroup import ProjectGroup
163+ conditions = []
164+ conditions = [Product.active]
165 if text:
166- queries.append("Product.fti @@ ftq(%s) " % sqlvalues(text))
167- if rosetta:
168- clauseTables.add('POTemplate')
169- clauseTables.add('ProductRelease')
170- clauseTables.add('ProductSeries')
171- queries.append("POTemplate.productrelease=ProductRelease.id")
172- queries.append("ProductRelease.productseries=ProductSeries.id")
173- queries.append("ProductSeries.product=product.id")
174- if malone:
175- clauseTables.add('BugTask')
176- queries.append('BugTask.product=Product.id')
177- if bazaar:
178- clauseTables.add('ProductSeries')
179- queries.append('(ProductSeries.branch IS NOT NULL)')
180- if 'ProductSeries' in clauseTables:
181- queries.append('ProductSeries.product=Product.id')
182- if not show_inactive:
183- queries.append('Product.active IS TRUE')
184- query = " AND ".join(queries)
185- return Product.select(query, distinct=True,
186- prejoins=["_owner"],
187- clauseTables=clauseTables)
188+ conditions.append(
189+ SQL("Product.fti @@ ftq(%s) " % sqlvalues(text)))
190+ result = IStore(Product).find(Product, *conditions)
191+ def eager_load(rows):
192+ product_ids = set(obj.id for obj in rows)
193+ if not product_ids:
194+ return
195+ products = dict((product.id, product) for product in rows)
196+ caches = dict((product.id, get_property_cache(product))
197+ for product in rows)
198+ for cache in caches.values():
199+ if not safe_hasattr(cache, 'commercial_subscription'):
200+ cache.commercial_subscription = None
201+ if not safe_hasattr(cache, '_cached_licenses'):
202+ cache._cached_licenses = []
203+ for subscription in IStore(CommercialSubscription).find(
204+ CommercialSubscription,
205+ CommercialSubscription.productID.is_in(product_ids)):
206+ cache = caches[subscription.productID]
207+ cache.commercial_subscription = subscription
208+ for license in IStore(ProductLicense).find(
209+ ProductLicense,
210+ ProductLicense.productID.is_in(product_ids)):
211+ cache = caches[license.productID]
212+ cache._cached_licenses.append(license.license)
213+ for cache in caches.values():
214+ cache._cached_licenses = tuple(sorted(cache._cached_licenses))
215+ bulk.load_related(ProjectGroup, products.values(), ['projectID'])
216+ bulk.load_related(ProductSeries, products.values(),
217+ ['development_focusID'])
218+ # Only need the objects for canonical_url, no need for validity.
219+ bulk.load_related(Person, products.values(),
220+ ['_ownerID', 'registrantID', 'bug_supervisorID', 'driverID',
221+ 'security_contactID'])
222+ return DecoratedResultSet(result, pre_iter_hook=eager_load)
223+
224+ def search_sqlobject(self, text):
225+ """See `IProductSet`"""
226+ clauseTables = ['Product']
227+ queries = ["Product.fti @@ ftq(%s) " % sqlvalues(text)]
228+ queries.append('Product.active IS TRUE')
229+ query = "Product.active IS TRUE AND Product.fti @@ ftq(%s)" \
230+ % sqlvalues(text)
231+ return Product.select(query)
232
233 def getTranslatables(self):
234 """See `IProductSet`"""