Merge lp:~jtv/launchpad/bug-618393 into lp:launchpad
- bug-618393
- Merge into devel
Status: | Merged |
---|---|
Approved by: | Jeroen T. Vermeulen |
Approved revision: | no longer in the source branch. |
Merged at revision: | 11498 |
Proposed branch: | lp:~jtv/launchpad/bug-618393 |
Merge into: | lp:launchpad |
Diff against target: |
804 lines (+361/-103) 10 files modified
lib/lp/registry/model/pillar.py (+7/-27) lib/lp/registry/model/product.py (+49/-5) lib/lp/registry/tests/test_productwithlicenses.py (+114/-0) lib/lp/translations/browser/tests/test_translationgroup.py (+2/-6) lib/lp/translations/browser/translationgroup.py (+4/-3) lib/lp/translations/doc/translationgroup.txt (+9/-7) lib/lp/translations/interfaces/translationgroup.py (+28/-2) lib/lp/translations/model/translationgroup.py (+114/-21) lib/lp/translations/templates/translationgroup-index.pt (+17/-16) lib/lp/translations/templates/translationgroup-portlet-projects.pt (+17/-16) |
To merge this branch: | bzr merge lp:~jtv/launchpad/bug-618393 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Henning Eggers (community) | code | Approve | |
Review via email: mp+34515@code.launchpad.net |
Commit message
TranslationGrou
Description of the change
= Bug 618393 =
Working on some timeouts on the TranslationGroup pages.
I looked at one oops in detail. A narrow majority of the time was taken up by database queries. So I mostly attacked those before resorting to python profiling.
Here I exploit several optimization opportunities:
* Prejoin icon data for persons, persons, etc. (20% of queries).
* Prejoin project license information (15% of queries).
* Cache whether user has Edit privileges in the view.
* Query translators once; avoid redundant is_empty check.
* Query for projects/project groups/
The most interesting work is on prefetching project license information. What makes this necessary is the link formatter for Product: it queries for licenses in order to flag projects whose licenses have not yet been set.
These are not queries one can avoid through Storm caching. Luckily there is a class that lets me get around that: ProductWithLice
You'll notice that I had to change the license_status property in ProductWithLice
To test,
{{{
./bin/test -vvc lp.translations -t translationgroup
./bin/test -vvc lp.registry -t productwithlicenses -t pillar.txt
}}}
To Q/A, go to
https:/
It should not time out. Also, the page should issue at most 600 or so queries, as opposed to the current 1,000.
No lint,
Jeroen
Henning Eggers (henninge) wrote : | # |
Hi Jeroen,
very nice branch and I learned a few new things by reviewing it. Thanks. ;-)
I could not find anything serious so please bear my nit-picks with patience.
And thank you for all the drive-by fixes! ;)
Cheers,
Henning
Am 03.09.2010 08:46, schrieb Jeroen T. Vermeulen:
> > === modified file 'lib/lp/
> > @@ -244,18 +230,12 @@
> > stacklevel=2)
> > pillars = []
> > # Prefill pillar.
> > - for pillar_name, other, product, project, distro, license_ids in (
> > + for pillar_name, other, product, project, distro, licenses in (
> > result[:limit]):
You can fix this confusing indention by pulling the de-tupling into the loop body.
> > pillar = pillar_name.pillar
> > if IProduct.
> > - licenses = [
> > - License.
> > - for license_id in license_ids]
> > - product_
> > - pillar, tuple(sorted(
> > - pillars.
> > - else:
> > - pillars.
> > + pillar = ProductWithLice
> > + pillars.
> > return pillars
> >
> > def add_featured_
> >
> > === modified file 'lib/lp/
> > @@ -201,14 +202,25 @@
> > return LicenseStatus.
> >
> >
> > +class Array(NamedFunc):
> > + """Implements the postgres "array" function in Storm."""
> > + name = 'array'
> > +
> > +
> > class ProductWithLice
> > """Caches `Product.
> >
> > delegates(IProduct, 'product')
> >
> > - def __init__(self, product, licenses):
> > + def __init__(self, product, license_ids):
> > + """Initialize a `ProductWithLic
> > +
> > + :param product: the `Product` to wrap.
> > + :param license_ids: a sequence of numeric `License` ids.
> > + """
> > self.product = product
> > - self._licenses = licenses
> > + self._licenses = tuple([
> > + License.items[id] for id in sorted(
You don't need the [] inside the tuple(). Go straight from iterator to tuple.
(I just found that out recently myself.)
> >
> > @property
> > def licenses(self):
> > === modified file 'lib/lp/
> > --- lib/lp/
> > +++ lib/lp/
> > @@ -186,8 +186,34 @@
> > def fetchTranslator
> > """Fetch translators and related data.
> >
> > - :return: A tuple (`Translator`, `Language`, `Person`), ordered
> > - by language name in English.
> > + Prefetches display-related properties.
> > +
> > + :return: A result set of (`Translator`, `Language`, `Person`),
> > + ordered by language name in English.
> > + """
> > +
> > + def fetchProjectsFo
I w...
Jeroen T. Vermeulen (jtv) wrote : | # |
Thanks. Wow, you did some careful research!
All your suggestions implemented, except moving the order_by out of the prejoin. (If nothing else, I like to avoid any risk of triggering query-before-slice bugs a priori). I didn't know tuple comprehensions were in python 2.5; I thought they were 2.6-only and so didn't use them.
You're also right that I shouldn't have said "Project" for "Product" in model code. It was reckless of me, and it is now undone.
Preview Diff
1 | === modified file 'lib/lp/registry/model/pillar.py' |
2 | --- lib/lp/registry/model/pillar.py 2010-08-20 20:31:18 +0000 |
3 | +++ lib/lp/registry/model/pillar.py 2010-09-03 11:18:46 +0000 |
4 | @@ -17,11 +17,7 @@ |
5 | ForeignKey, |
6 | StringCol, |
7 | ) |
8 | -from storm.expr import ( |
9 | - LeftJoin, |
10 | - NamedFunc, |
11 | - Select, |
12 | - ) |
13 | +from storm.expr import LeftJoin |
14 | from storm.info import ClassAlias |
15 | from storm.locals import SQL |
16 | from storm.store import Store |
17 | @@ -51,11 +47,9 @@ |
18 | from lp.registry.interfaces.product import ( |
19 | IProduct, |
20 | IProductSet, |
21 | - License, |
22 | ) |
23 | from lp.registry.interfaces.projectgroup import IProjectGroupSet |
24 | from lp.registry.model.featuredproject import FeaturedProject |
25 | -from lp.registry.model.productlicense import ProductLicense |
26 | |
27 | |
28 | __all__ = [ |
29 | @@ -197,23 +191,15 @@ |
30 | |
31 | def search(self, text, limit): |
32 | """See `IPillarSet`.""" |
33 | - # These classes are imported in this method to prevent an import loop. |
34 | - from lp.registry.model.product import ( |
35 | - Product, ProductWithLicenses) |
36 | + # Avoid circular import. |
37 | + from lp.registry.model.product import ProductWithLicenses |
38 | if limit is None: |
39 | limit = config.launchpad.default_batch_size |
40 | |
41 | - class Array(NamedFunc): |
42 | - """Storm representation of the array() PostgreSQL function.""" |
43 | - name = 'array' |
44 | - |
45 | # Pull out the licenses as a subselect which is converted |
46 | # into a PostgreSQL array so that multiple licenses per product |
47 | # can be retrieved in a single row for each product. |
48 | - extra_column = Array( |
49 | - Select(columns=[ProductLicense.license], |
50 | - where=(ProductLicense.product == Product.id), |
51 | - tables=[ProductLicense])) |
52 | + extra_column = ProductWithLicenses.composeLicensesColumn() |
53 | result = self.build_search_query(text, [extra_column]) |
54 | |
55 | # If the search text matches the name or title of the |
56 | @@ -244,18 +230,12 @@ |
57 | stacklevel=2) |
58 | pillars = [] |
59 | # Prefill pillar.product.licenses. |
60 | - for pillar_name, other, product, project, distro, license_ids in ( |
61 | + for pillar_name, other, product, project, distro, licenses in ( |
62 | result[:limit]): |
63 | pillar = pillar_name.pillar |
64 | if IProduct.providedBy(pillar): |
65 | - licenses = [ |
66 | - License.items[license_id] |
67 | - for license_id in license_ids] |
68 | - product_with_licenses = ProductWithLicenses( |
69 | - pillar, tuple(sorted(licenses))) |
70 | - pillars.append(product_with_licenses) |
71 | - else: |
72 | - pillars.append(pillar) |
73 | + pillar = ProductWithLicenses(pillar, licenses) |
74 | + pillars.append(pillar) |
75 | return pillars |
76 | |
77 | def add_featured_project(self, project): |
78 | |
79 | === modified file 'lib/lp/registry/model/product.py' |
80 | --- lib/lp/registry/model/product.py 2010-09-03 03:12:39 +0000 |
81 | +++ lib/lp/registry/model/product.py 2010-09-03 11:18:46 +0000 |
82 | @@ -1,4 +1,4 @@ |
83 | -# Copyright 2009 Canonical Ltd. This software is licensed under the |
84 | +# Copyright 2009-2010 Canonical Ltd. This software is licensed under the |
85 | # GNU Affero General Public License version 3 (see the file LICENSE). |
86 | # pylint: disable-msg=E0611,W0212 |
87 | |
88 | @@ -25,11 +25,13 @@ |
89 | SQLObjectNotFound, |
90 | StringCol, |
91 | ) |
92 | +from storm.expr import NamedFunc |
93 | from storm.locals import ( |
94 | And, |
95 | Desc, |
96 | Int, |
97 | Join, |
98 | + Select, |
99 | SQL, |
100 | Store, |
101 | Unicode, |
102 | @@ -85,7 +87,6 @@ |
103 | SpecificationDefinitionStatus, |
104 | SpecificationFilter, |
105 | SpecificationImplementationStatus, |
106 | - SpecificationSort, |
107 | ) |
108 | from lp.blueprints.model.specification import ( |
109 | HasSpecificationsMixin, |
110 | @@ -201,14 +202,25 @@ |
111 | return LicenseStatus.OPEN_SOURCE |
112 | |
113 | |
114 | +class Array(NamedFunc): |
115 | + """Implements the postgres "array" function in Storm.""" |
116 | + name = 'array' |
117 | + |
118 | + |
119 | class ProductWithLicenses: |
120 | """Caches `Product.licenses`.""" |
121 | |
122 | delegates(IProduct, 'product') |
123 | |
124 | - def __init__(self, product, licenses): |
125 | + def __init__(self, product, license_ids): |
126 | + """Initialize a `ProductWithLicenses`. |
127 | + |
128 | + :param product: the `Product` to wrap. |
129 | + :param license_ids: a sequence of numeric `License` ids. |
130 | + """ |
131 | self.product = product |
132 | - self._licenses = licenses |
133 | + self._licenses = tuple([ |
134 | + License.items[id] for id in sorted(license_ids)]) |
135 | |
136 | @property |
137 | def licenses(self): |
138 | @@ -223,8 +235,40 @@ |
139 | `Product.licenses`, which is not cached, instead of |
140 | `ProductWithLicenses.licenses`, which is cached. |
141 | """ |
142 | + naked_product = removeSecurityProxy(self.product) |
143 | return get_license_status( |
144 | - self.license_approved, self.license_reviewed, self.licenses) |
145 | + naked_product.license_approved, naked_product.license_reviewed, |
146 | + self.licenses) |
147 | + |
148 | + @classmethod |
149 | + def composeLicensesColumn(cls, for_class=None): |
150 | + """Compose a Storm column specification for licenses. |
151 | + |
152 | + Use this to render a list of `Product` linkes without querying |
153 | + licenses for each one individually. |
154 | + |
155 | + It lets you prefetch the licensing information in the same |
156 | + query that fetches a `Product`. Just add the column spec |
157 | + returned by this function to the query, and pass it to the |
158 | + `ProductWithLicenses` constructor: |
159 | + |
160 | + license_column = ProductWithLicenses.composeLicensesColumn() |
161 | + products_with_licenses = [ |
162 | + ProductWithLicenses(product, licenses) |
163 | + for product, licenses in store.find(Product, license_column) |
164 | + ] |
165 | + |
166 | + :param for_class: Class to find licenses for. Defaults to |
167 | + `Product`, but could also be a Storm `ClassAlias`. |
168 | + """ |
169 | + if for_class is None: |
170 | + for_class = Product |
171 | + |
172 | + return Array( |
173 | + Select( |
174 | + columns=[ProductLicense.license], |
175 | + where=(ProductLicense.product == for_class.id), |
176 | + tables=[ProductLicense])) |
177 | |
178 | |
179 | class Product(SQLBase, BugTargetBase, MakesAnnouncements, |
180 | |
181 | === added file 'lib/lp/registry/tests/test_productwithlicenses.py' |
182 | --- lib/lp/registry/tests/test_productwithlicenses.py 1970-01-01 00:00:00 +0000 |
183 | +++ lib/lp/registry/tests/test_productwithlicenses.py 2010-09-03 11:18:46 +0000 |
184 | @@ -0,0 +1,114 @@ |
185 | +# Copyright 2010 Canonical Ltd. This software is licensed under the |
186 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
187 | + |
188 | +"""Unit tests for `ProductWithLicenses`.""" |
189 | + |
190 | +__metaclass__ = type |
191 | + |
192 | +from operator import attrgetter |
193 | + |
194 | +from storm.store import Store |
195 | +from zope.interface.verify import verifyObject |
196 | + |
197 | +from canonical.launchpad.ftests import ( |
198 | + ANONYMOUS, |
199 | + login, |
200 | + ) |
201 | +from canonical.testing import DatabaseFunctionalLayer |
202 | +from lp.registry.interfaces.product import ( |
203 | + IProduct, |
204 | + License, |
205 | + LicenseStatus, |
206 | + ) |
207 | +from lp.registry.model.product import ( |
208 | + Product, |
209 | + ProductWithLicenses, |
210 | + ) |
211 | +from lp.testing import TestCaseWithFactory |
212 | + |
213 | + |
214 | +class TestProductWithLicenses(TestCaseWithFactory): |
215 | + |
216 | + layer = DatabaseFunctionalLayer |
217 | + |
218 | + def test_baseline(self): |
219 | + product = self.factory.makeProduct() |
220 | + product_with_licenses = ProductWithLicenses(product, []) |
221 | + # Log in--a full verification takes Edit privileges. |
222 | + login('foo.bar@canonical.com') |
223 | + self.assertTrue(verifyObject(IProduct, product_with_licenses)) |
224 | + |
225 | + def test_uses_cached_licenses(self): |
226 | + # The ProductWithLicenses' licensing information is based purely |
227 | + # on the cached licenses list. The database is not queried to |
228 | + # determine licensing status. |
229 | + product = self.factory.makeProduct(licenses=[License.BSD]) |
230 | + product_with_licenses = ProductWithLicenses( |
231 | + product, [License.OTHER_PROPRIETARY.value]) |
232 | + license_status = product_with_licenses.license_status |
233 | + self.assertEqual(LicenseStatus.PROPRIETARY, license_status) |
234 | + |
235 | + def test_sorts_licenses(self): |
236 | + # The ProductWithLicenses constructor sorts the Licenses by |
237 | + # numeric value. |
238 | + product = self.factory.makeProduct() |
239 | + licenses = [License.AFFERO, License.BSD, License.MIT] |
240 | + |
241 | + # Feed the constructor a list of ids in the wrong order. |
242 | + product_with_licenses = ProductWithLicenses( |
243 | + product, |
244 | + sorted([license.value for license in licenses], reverse=True)) |
245 | + |
246 | + expected = sorted(licenses, key=attrgetter('value')) |
247 | + self.assertEqual(tuple(expected), product_with_licenses.licenses) |
248 | + |
249 | + def test_compose_column_without_licenses_produces_empty(self): |
250 | + # The licenses column that ProductWithLicenses produces for a |
251 | + # product without licenses contains an empty list. |
252 | + product = self.factory.makeProduct(licenses=[]) |
253 | + column = ProductWithLicenses.composeLicensesColumn() |
254 | + store = Store.of(product) |
255 | + result = list(store.find((Product, column), Product.id == product.id)) |
256 | + |
257 | + self.assertEqual([(product, [])], result) |
258 | + |
259 | + def test_licenses_column_contains_licensing_info(self): |
260 | + # Feeding the licenses column into the ProductWithLicenses |
261 | + # constructor seeds it with the appropriate licenses. |
262 | + product = self.factory.makeProduct( |
263 | + licenses=[License.OTHER_PROPRIETARY]) |
264 | + column = ProductWithLicenses.composeLicensesColumn() |
265 | + store = Store.of(product) |
266 | + row = store.find((Product, column), Product.id == product.id).one() |
267 | + |
268 | + product_with_licenses = ProductWithLicenses(*row) |
269 | + licenses = product_with_licenses.licenses |
270 | + license_status = product_with_licenses.license_status |
271 | + self.assertEqual((License.OTHER_PROPRIETARY, ), licenses) |
272 | + self.assertEqual(LicenseStatus.PROPRIETARY, license_status) |
273 | + |
274 | + def test_licenses_column_aggregates(self): |
275 | + # Adding a licensing column for a product with multiple licenses |
276 | + # still finds a single product, not one per license. |
277 | + licenses = [License.AFFERO, License.GNU_GPL_V3] |
278 | + product = self.factory.makeProduct(licenses=licenses) |
279 | + column = ProductWithLicenses.composeLicensesColumn() |
280 | + store = Store.of(product) |
281 | + result = list(store.find((Product, column), Product.id == product.id)) |
282 | + |
283 | + self.assertEqual(1, len(result)) |
284 | + found_product, found_licenses = result[0] |
285 | + self.assertEqual(product, found_product) |
286 | + self.assertEqual(len(licenses), len(found_licenses)) |
287 | + |
288 | + def test_license_status_is_public(self): |
289 | + # The license_status attribute can be read by anyone, on |
290 | + # ProductWithLicenses as on Product. |
291 | + product = self.factory.makeProduct(licenses=[License.BSD]) |
292 | + product_with_licenses = ProductWithLicenses( |
293 | + product, [License.BSD.value]) |
294 | + login(ANONYMOUS) |
295 | + self.assertEqual( |
296 | + LicenseStatus.OPEN_SOURCE, product.license_status) |
297 | + self.assertEqual( |
298 | + LicenseStatus.OPEN_SOURCE, product_with_licenses.license_status) |
299 | |
300 | === modified file 'lib/lp/translations/browser/tests/test_translationgroup.py' |
301 | --- lib/lp/translations/browser/tests/test_translationgroup.py 2010-08-20 20:31:18 +0000 |
302 | +++ lib/lp/translations/browser/tests/test_translationgroup.py 2010-09-03 11:18:46 +0000 |
303 | @@ -5,8 +5,7 @@ |
304 | |
305 | __metaclass__ = type |
306 | |
307 | -import unittest |
308 | - |
309 | +import transaction |
310 | from zope.component import getUtility |
311 | |
312 | from canonical.launchpad.webapp.servers import LaunchpadTestRequest |
313 | @@ -40,6 +39,7 @@ |
314 | # translator_list composes dicts using _makeTranslatorDict. |
315 | group = self._makeGroup() |
316 | tr_translator = self.factory.makeTranslator('tr', group) |
317 | + transaction.commit() |
318 | view = self._makeView(group) |
319 | translator_dict = view._makeTranslatorDict( |
320 | tr_translator, tr_translator.language, tr_translator.translator) |
321 | @@ -62,7 +62,3 @@ |
322 | self.assertEqual(xhosa.datecreated, output['datecreated']) |
323 | self.assertEqual(xhosa.style_guide_url, output['style_guide_url']) |
324 | self.assertEqual(xhosa, output['context']) |
325 | - |
326 | - |
327 | -def test_suite(): |
328 | - return unittest.TestLoader().loadTestsFromName(__name__) |
329 | |
330 | === modified file 'lib/lp/translations/browser/translationgroup.py' |
331 | --- lib/lp/translations/browser/translationgroup.py 2010-08-20 20:31:18 +0000 |
332 | +++ lib/lp/translations/browser/translationgroup.py 2010-09-03 11:18:46 +0000 |
333 | @@ -1,4 +1,4 @@ |
334 | -# Copyright 2009 Canonical Ltd. This software is licensed under the |
335 | +# Copyright 2009-2010 Canonical Ltd. This software is licensed under the |
336 | # GNU Affero General Public License version 3 (see the file LICENSE). |
337 | |
338 | """Browser code for translation groups.""" |
339 | @@ -26,6 +26,7 @@ |
340 | LaunchpadFormView, |
341 | LaunchpadView, |
342 | ) |
343 | +from canonical.launchpad.webapp.authorization import check_permission |
344 | from canonical.launchpad.webapp.breadcrumb import Breadcrumb |
345 | from lp.app.errors import NotFoundError |
346 | from lp.registry.browser.objectreassignment import ObjectReassignmentView |
347 | @@ -67,6 +68,7 @@ |
348 | self.context = context |
349 | self.request = request |
350 | self.translation_groups = getUtility(ITranslationGroupSet) |
351 | + self.user_can_edit = check_permission('launchpad.Edit', self.context) |
352 | |
353 | @property |
354 | def label(self): |
355 | @@ -96,8 +98,7 @@ |
356 | """List of dicts describing the translation teams.""" |
357 | return [ |
358 | self._makeTranslatorDict(*data) |
359 | - for data in self.context.fetchTranslatorData() |
360 | - ] |
361 | + for data in self.context.fetchTranslatorData()] |
362 | |
363 | |
364 | class TranslationGroupAddTranslatorView(LaunchpadFormView): |
365 | |
366 | === modified file 'lib/lp/translations/doc/translationgroup.txt' |
367 | --- lib/lp/translations/doc/translationgroup.txt 2010-02-22 09:06:38 +0000 |
368 | +++ lib/lp/translations/doc/translationgroup.txt 2010-09-03 11:18:46 +0000 |
369 | @@ -1,4 +1,5 @@ |
370 | -= Translation Groups = |
371 | +Translation Groups |
372 | +================== |
373 | |
374 | >>> from zope.component import getUtility |
375 | |
376 | @@ -63,12 +64,12 @@ |
377 | |
378 | No Privileges Person isn't allowed to translate into Welsh. |
379 | |
380 | - >>> series = evolution.getSeries('trunk') |
381 | - >>> subset = potemplate_set.getSubset(productseries=series) |
382 | - >>> potemplate = subset['evolution-2.2'] |
383 | - >>> pofile = potemplate.newPOFile('cy') |
384 | - >>> pofile.canEditTranslations(no_priv) |
385 | - False |
386 | + >>> series = evolution.getSeries('trunk') |
387 | + >>> subset = potemplate_set.getSubset(productseries=series) |
388 | + >>> potemplate = subset['evolution-2.2'] |
389 | + >>> pofile = potemplate.newPOFile('cy') |
390 | + >>> pofile.canEditTranslations(no_priv) |
391 | + False |
392 | |
393 | Let's add him to the group. |
394 | |
395 | @@ -176,6 +177,7 @@ |
396 | >>> de_translator = factory.makeTranslator('de', group, person=de_team) |
397 | >>> nl_translator = factory.makeTranslator('nl', group, person=nl_team) |
398 | >>> la_translator = factory.makeTranslator('la', group, person=la_team) |
399 | + >>> transaction.commit() |
400 | |
401 | The method returns tuples of respectively a Translator ("translation |
402 | group membership entry"), its language, and the actual team or person |
403 | |
404 | === modified file 'lib/lp/translations/interfaces/translationgroup.py' |
405 | --- lib/lp/translations/interfaces/translationgroup.py 2010-08-20 20:31:18 +0000 |
406 | +++ lib/lp/translations/interfaces/translationgroup.py 2010-09-03 11:18:46 +0000 |
407 | @@ -186,8 +186,34 @@ |
408 | def fetchTranslatorData(): |
409 | """Fetch translators and related data. |
410 | |
411 | - :return: A tuple (`Translator`, `Language`, `Person`), ordered |
412 | - by language name in English. |
413 | + Prefetches display-related properties. |
414 | + |
415 | + :return: A result set of (`Translator`, `Language`, `Person`), |
416 | + ordered by language name in English. |
417 | + """ |
418 | + |
419 | + def fetchProjectsForDisplay(): |
420 | + """Fetch `Product`s using this group, for display purposes. |
421 | + |
422 | + Prefetches display-related properties. |
423 | + |
424 | + :return: A result set of `Product`, ordered by display name. |
425 | + """ |
426 | + |
427 | + def fetchProjectGroupsForDisplay(): |
428 | + """Fetch `Project`s using this group, for display purposes. |
429 | + |
430 | + Prefetches display-related properties. |
431 | + |
432 | + :return: A result set of `Project`, ordered by display name. |
433 | + """ |
434 | + |
435 | + def fetchDistrosForDisplay(): |
436 | + """Fetch `Distribution`s using this group, for display purposes. |
437 | + |
438 | + Prefetches display-related properties. |
439 | + |
440 | + :return: A result set of `Distribution`, ordered by display name. |
441 | """ |
442 | |
443 | |
444 | |
445 | === modified file 'lib/lp/translations/model/translationgroup.py' |
446 | --- lib/lp/translations/model/translationgroup.py 2010-08-20 20:31:18 +0000 |
447 | +++ lib/lp/translations/model/translationgroup.py 2010-09-03 11:18:46 +0000 |
448 | @@ -1,4 +1,4 @@ |
449 | -# Copyright 2009 Canonical Ltd. This software is licensed under the |
450 | +# Copyright 2009-2010 Canonical Ltd. This software is licensed under the |
451 | # GNU Affero General Public License version 3 (see the file LICENSE). |
452 | |
453 | # pylint: disable-msg=E0611,W0212 |
454 | @@ -6,7 +6,7 @@ |
455 | __metaclass__ = type |
456 | __all__ = [ |
457 | 'TranslationGroup', |
458 | - 'TranslationGroupSet' |
459 | + 'TranslationGroupSet', |
460 | ] |
461 | |
462 | from sqlobject import ( |
463 | @@ -16,25 +16,32 @@ |
464 | SQLRelatedJoin, |
465 | StringCol, |
466 | ) |
467 | -from storm.expr import Join |
468 | +from storm.expr import ( |
469 | + Join, |
470 | + LeftJoin, |
471 | + ) |
472 | from storm.store import Store |
473 | -from zope.component import getUtility |
474 | from zope.interface import implements |
475 | |
476 | from canonical.database.constants import DEFAULT |
477 | from canonical.database.datetimecol import UtcDateTimeCol |
478 | from canonical.database.sqlbase import SQLBase |
479 | -from canonical.launchpad.webapp.interfaces import ( |
480 | - DEFAULT_FLAVOR, |
481 | - IStoreSelector, |
482 | - MAIN_STORE, |
483 | +from canonical.launchpad.database.librarian import ( |
484 | + LibraryFileAlias, |
485 | + LibraryFileContent, |
486 | ) |
487 | +from canonical.launchpad.interfaces.lpstorm import ISlaveStore |
488 | from lp.app.errors import NotFoundError |
489 | from lp.registry.interfaces.person import validate_public_person |
490 | +from lp.registry.model.distribution import Distribution |
491 | from lp.registry.model.person import Person |
492 | -from lp.registry.model.product import Product |
493 | +from lp.registry.model.product import ( |
494 | + Product, |
495 | + ProductWithLicenses, |
496 | + ) |
497 | from lp.registry.model.projectgroup import ProjectGroup |
498 | from lp.registry.model.teammembership import TeamParticipation |
499 | +from lp.services.database.prejoin import prejoin |
500 | from lp.services.worlddata.model.language import Language |
501 | from lp.translations.interfaces.translationgroup import ( |
502 | ITranslationGroup, |
503 | @@ -71,16 +78,15 @@ |
504 | |
505 | def __getitem__(self, language_code): |
506 | """See `ITranslationGroup`.""" |
507 | - store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR) |
508 | - query = store.find( |
509 | + query = Store.of(self).find( |
510 | Translator, |
511 | Translator.translationgroup == self, |
512 | Translator.languageID == Language.id, |
513 | Language.code == language_code) |
514 | - |
515 | + |
516 | translator = query.one() |
517 | if translator is None: |
518 | - raise NotFoundError, language_code |
519 | + raise NotFoundError(language_code) |
520 | |
521 | return translator |
522 | |
523 | @@ -141,14 +147,102 @@ |
524 | return 0 |
525 | |
526 | def fetchTranslatorData(self): |
527 | - """See ITranslationGroup.""" |
528 | - store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR) |
529 | - translator_data = store.find( |
530 | - (Translator, Language, Person), |
531 | + """See `ITranslationGroup`.""" |
532 | + # Fetch Translator, Language, and Person; but also prefetch the |
533 | + # icon information. |
534 | + using = [ |
535 | + Translator, |
536 | + Language, |
537 | + Person, |
538 | + LeftJoin(LibraryFileAlias, LibraryFileAlias.id == Person.iconID), |
539 | + LeftJoin( |
540 | + LibraryFileContent, |
541 | + LibraryFileContent.id == LibraryFileAlias.contentID), |
542 | + ] |
543 | + tables = ( |
544 | + Translator, |
545 | + Language, |
546 | + Person, |
547 | + LibraryFileAlias, |
548 | + LibraryFileContent, |
549 | + ) |
550 | + translator_data = Store.of(self).using(*using).find( |
551 | + tables, |
552 | Translator.translationgroup == self, |
553 | Language.id == Translator.languageID, |
554 | Person.id == Translator.translatorID) |
555 | - return translator_data.order_by(Language.englishname) |
556 | + |
557 | + return prejoin( |
558 | + translator_data.order_by(Language.englishname), |
559 | + slice(0, 3)) |
560 | + |
561 | + def fetchProjectsForDisplay(self): |
562 | + """See `ITranslationGroup`.""" |
563 | + using = [ |
564 | + Product, |
565 | + LeftJoin(LibraryFileAlias, LibraryFileAlias.id == Product.iconID), |
566 | + LeftJoin( |
567 | + LibraryFileContent, |
568 | + LibraryFileContent.id == LibraryFileAlias.contentID), |
569 | + ] |
570 | + columns = ( |
571 | + Product, |
572 | + ProductWithLicenses.composeLicensesColumn(), |
573 | + LibraryFileAlias, |
574 | + LibraryFileContent, |
575 | + ) |
576 | + product_data = ISlaveStore(Product).using(*using).find( |
577 | + columns, |
578 | + Product.translationgroupID == self.id, Product.active == True) |
579 | + product_data = product_data.order_by(Product.displayname) |
580 | + |
581 | + return [ |
582 | + ProductWithLicenses(product, tuple(licenses)) |
583 | + for product, licenses, icon_alias, icon_content in product_data] |
584 | + |
585 | + def fetchProjectGroupsForDisplay(self): |
586 | + """See `ITranslationGroup`.""" |
587 | + using = [ |
588 | + ProjectGroup, |
589 | + LeftJoin( |
590 | + LibraryFileAlias, LibraryFileAlias.id == ProjectGroup.iconID), |
591 | + LeftJoin( |
592 | + LibraryFileContent, |
593 | + LibraryFileContent.id == LibraryFileAlias.contentID), |
594 | + ] |
595 | + tables = ( |
596 | + ProjectGroup, |
597 | + LibraryFileAlias, |
598 | + LibraryFileContent, |
599 | + ) |
600 | + project_data = ISlaveStore(ProjectGroup).using(*using).find( |
601 | + tables, |
602 | + ProjectGroup.translationgroupID == self.id, |
603 | + ProjectGroup.active == True) |
604 | + |
605 | + return prejoin( |
606 | + project_data.order_by(ProjectGroup.displayname), slice(0, 1)) |
607 | + |
608 | + def fetchDistrosForDisplay(self): |
609 | + """See `ITranslationGroup`.""" |
610 | + using = [ |
611 | + Distribution, |
612 | + LeftJoin( |
613 | + LibraryFileAlias, LibraryFileAlias.id == Distribution.iconID), |
614 | + LeftJoin( |
615 | + LibraryFileContent, |
616 | + LibraryFileContent.id == LibraryFileAlias.contentID), |
617 | + ] |
618 | + tables = ( |
619 | + Distribution, |
620 | + LibraryFileAlias, |
621 | + LibraryFileContent, |
622 | + ) |
623 | + distro_data = ISlaveStore(Distribution).using(*using).find( |
624 | + tables, Distribution.translationgroupID == self.id) |
625 | + |
626 | + return prejoin( |
627 | + distro_data.order_by(Distribution.displayname), slice(0, 1)) |
628 | |
629 | |
630 | class TranslationGroupSet: |
631 | @@ -174,7 +268,7 @@ |
632 | try: |
633 | return TranslationGroup.byName(name) |
634 | except SQLObjectNotFound: |
635 | - raise NotFoundError, name |
636 | + raise NotFoundError(name) |
637 | |
638 | def new(self, name, title, summary, translation_guide_url, owner): |
639 | """See ITranslationGroupSet.""" |
640 | @@ -193,7 +287,7 @@ |
641 | Join(Translator, |
642 | Translator.translationgroupID == TranslationGroup.id), |
643 | Join(TeamParticipation, |
644 | - TeamParticipation.teamID == Translator.translatorID) |
645 | + TeamParticipation.teamID == Translator.translatorID), |
646 | ] |
647 | result = store.using(*origin).find( |
648 | TranslationGroup, TeamParticipation.person == person) |
649 | @@ -203,4 +297,3 @@ |
650 | def getGroupsCount(self): |
651 | """See ITranslationGroupSet.""" |
652 | return TranslationGroup.select().count() |
653 | - |
654 | |
655 | === modified file 'lib/lp/translations/templates/translationgroup-index.pt' |
656 | --- lib/lp/translations/templates/translationgroup-index.pt 2010-02-22 09:06:38 +0000 |
657 | +++ lib/lp/translations/templates/translationgroup-index.pt 2010-09-03 11:18:46 +0000 |
658 | @@ -13,7 +13,7 @@ |
659 | <a tal:content="context/owner/fmt:displayname" |
660 | tal:attributes="href context/owner/fmt:url" |
661 | class="link" /> |
662 | - <a tal:condition="context/required:launchpad.Edit" |
663 | + <a tal:condition="view/user_can_edit" |
664 | href="+reassign" title="Change administrator" |
665 | class="edit sprite" id="link-reassign"></a> |
666 | </p> |
667 | @@ -32,7 +32,7 @@ |
668 | translations in this translation group.</em> |
669 | </p> |
670 | |
671 | - <div tal:condition="context/required:launchpad.Edit"> |
672 | + <div tal:condition="view/user_can_edit"> |
673 | <a href="+edit" class="edit sprite">Change details</a> |
674 | </div> |
675 | |
676 | @@ -40,7 +40,8 @@ |
677 | |
678 | <div id="translation-teams-listing"> |
679 | <h2><a name="teams"></a>Translation teams</h2> |
680 | - <tal:translators condition="context/translators"> |
681 | + <tal:translator-list define="translator_list view/translator_list"> |
682 | + <tal:translators condition="translator_list"> |
683 | <table class="sortable listing" id="group-members"> |
684 | <thead> |
685 | <tr> |
686 | @@ -48,11 +49,11 @@ |
687 | <th>Team/Supervisor</th> |
688 | <th>Guidelines</th> |
689 | <th>Appointed</th> |
690 | - <th tal:condition="context/required:launchpad.Edit"></th> |
691 | + <th tal:condition="view/user_can_edit"></th> |
692 | </tr> |
693 | </thead> |
694 | <tbody> |
695 | - <tr tal:repeat="translator view/translator_list"> |
696 | + <tr tal:repeat="translator translator_list"> |
697 | <td class="translator-language"> |
698 | <a tal:attributes="href translator/language/fmt:url" |
699 | tal:content="translator/language/displayname"> |
700 | @@ -82,7 +83,7 @@ |
701 | none |
702 | </span> |
703 | <tal:notadmin |
704 | - condition="not:context/required:launchpad.Edit"> |
705 | + condition="not:view/user_can_edit"> |
706 | <a tal:condition=" |
707 | translator/context/required:launchpad.Edit" |
708 | tal:attributes="href string:${translator/code}/+edit" |
709 | @@ -96,7 +97,7 @@ |
710 | 2007-09-17 |
711 | </span> |
712 | </td> |
713 | - <td tal:condition="context/required:launchpad.Edit"> |
714 | + <td tal:condition="view/user_can_edit"> |
715 | <a tal:attributes=" |
716 | href translator/code; |
717 | id string:edit-${translator/code}-translator" |
718 | @@ -110,22 +111,22 @@ |
719 | </tbody> |
720 | </table> |
721 | </tal:translators> |
722 | - <tal:no-translators condition="not: context/translators"> |
723 | + <tal:no-translators condition="not: translator_list"> |
724 | No translation teams or supervisors have been appointed in |
725 | this group yet. |
726 | </tal:no-translators> |
727 | <div style="margin-top:1em; margin-bottom: 2em;"> |
728 | - <a tal:condition="context/required:launchpad.Edit" |
729 | + <a tal:condition="view/user_can_edit" |
730 | href="+appoint" class="add sprite">Appoint a new translation |
731 | team</a> |
732 | </div> |
733 | - </div><!-- id="translations-team-listing" --> |
734 | - |
735 | - <div class="section"> |
736 | - <a name="projects"></a> |
737 | - <div tal:replace="structure context/@@+portlet-projects" /> |
738 | - </div> |
739 | - |
740 | + </tal:translator-list> |
741 | + </div><!-- id="translations-team-listing" --> |
742 | + |
743 | + <div class="section"> |
744 | + <a name="projects"></a> |
745 | + <div tal:replace="structure context/@@+portlet-projects" /> |
746 | + </div> |
747 | </div><!-- main --> |
748 | |
749 | </body> |
750 | |
751 | === modified file 'lib/lp/translations/templates/translationgroup-portlet-projects.pt' |
752 | --- lib/lp/translations/templates/translationgroup-portlet-projects.pt 2010-04-19 08:11:52 +0000 |
753 | +++ lib/lp/translations/templates/translationgroup-portlet-projects.pt 2010-09-03 11:18:46 +0000 |
754 | @@ -13,33 +13,34 @@ |
755 | <a href="#teams">translation teams</a>. |
756 | </p> |
757 | |
758 | - <div id="related-projects"> |
759 | - <div tal:condition="context/distributions" style="margin-top:1em;"> |
760 | + <div id="related-projects" |
761 | + tal:define=" |
762 | + distributions context/fetchDistrosForDisplay; |
763 | + projectgroups context/fetchProjectGroupsForDisplay; |
764 | + projects context/fetchProjectsForDisplay"> |
765 | + <div tal:condition="distributions" style="margin-top:1em;"> |
766 | <h3 style="display: inline;">Distributions:</h3> |
767 | - <tal:distribution |
768 | - repeat="distribution context/distributions"> |
769 | + <tal:distribution repeat="distribution distributions"> |
770 | <a href="#" tal:replace="structure distribution/fmt:link">Ubuntu |
771 | </a><tal:comma condition="not:repeat/distribution/end">, </tal:comma> |
772 | </tal:distribution> |
773 | </div> |
774 | |
775 | - <div tal:condition="context/projects" style="margin-top:1em;"> |
776 | + <div tal:condition="projectgroups" style="margin-top:1em;"> |
777 | <h3 style="display: inline;">Project groups:</h3> |
778 | - <tal:project |
779 | - repeat="projectgroup context/projects"> |
780 | + <tal:projectgroup repeat="projectgroup projectgroups"> |
781 | <a href="#" tal:replace="structure projectgroup/fmt:link">GNOME |
782 | </a><tal:comma condition="not:repeat/projectgroup/end">, </tal:comma> |
783 | + </tal:projectgroup> |
784 | + </div> |
785 | + |
786 | + <div tal:condition="projects" style="margin-top:1em;"> |
787 | + <h3 style="display: inline;">Projects:</h3> |
788 | + <tal:project repeat="project projects"> |
789 | + <a href="#" tal:replace="structure project/fmt:link">Firefox |
790 | + </a><tal:comma condition="not:repeat/project/end">, </tal:comma> |
791 | </tal:project> |
792 | </div> |
793 | |
794 | - <div tal:condition="context/products" style="margin-top:1em;"> |
795 | - <h3 style="display: inline;">Projects:</h3> |
796 | - <tal:product |
797 | - repeat="product context/products"> |
798 | - <a href="#" tal:replace="structure product/fmt:link">Firefox |
799 | - </a><tal:comma condition="not:repeat/product/end">, </tal:comma> |
800 | - </tal:product> |
801 | - </div> |
802 | - |
803 | </div> |
804 | </tal:root> |
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1
Hi Jeroen,
very nice branch and I learned a few new things by reviewing it. Thanks. ;-)
I could not find anything serious so please bear my nit-picks with patience.
And thank you for all the drive-by fixes! ;)
review approve code
Cheers,
Henning
Am 03.09.2010 08:46, schrieb Jeroen T. Vermeulen: registry/ model/pillar. py' product. licenses.
> === modified file 'lib/lp/
> @@ -244,18 +230,12 @@
> stacklevel=2)
> pillars = []
> # Prefill pillar.
> - for pillar_name, other, product, project, distro, license_ids in (
> + for pillar_name, other, product, project, distro, licenses in (
> result[:limit]):
You can fix this confusing indention by pulling the de-tupling into the loop body.
> pillar = pillar_name.pillar providedBy( pillar) : items[license_ id] with_licenses = ProductWithLice nses( licenses) )) append( product_ with_licenses) append( pillar) nses(pillar, licenses) append( pillar) project( self, project): registry/ model/product. py' OPEN_SOURCE nses: licenses` .""" enses`. license_ ids)])
> if IProduct.
> - licenses = [
> - License.
> - for license_id in license_ids]
> - product_
> - pillar, tuple(sorted(
> - pillars.
> - else:
> - pillars.
> + pillar = ProductWithLice
> + pillars.
> return pillars
>
> def add_featured_
>
> === modified file 'lib/lp/
> @@ -201,14 +202,25 @@
> return LicenseStatus.
>
>
> +class Array(NamedFunc):
> + """Implements the postgres "array" function in Storm."""
> + name = 'array'
> +
> +
> class ProductWithLice
> """Caches `Product.
>
> delegates(IProduct, 'product')
>
> - def __init__(self, product, licenses):
> + def __init__(self, product, license_ids):
> + """Initialize a `ProductWithLic
> +
> + :param product: the `Product` to wrap.
> + :param license_ids: a sequence of numeric `License` ids.
> + """
> self.product = product
> - self._licenses = licenses
> + self._licenses = tuple([
> + License.items[id] for id in sorted(
You don't need the [] inside the tuple(). Go straight from iterator to tuple.
(I just found that out recently myself.)
>
> @property
> def licenses(self):
> === modified file 'lib/lp/ translations/ interfaces/ translationgrou p.py' translations/ interfaces/ translationgrou p.py 2010-08-20 20:31:18 +0000 translations/ interfaces/ translationgrou p.py 2010-09-03 06:46:46 +0000 Data(): rDisplay( ):
> --- lib/lp/
> +++ lib/lp/
> @@ -186,8 +186,34 @@
> def fetchTranslator
> """Fetch translators and related data.
>
> - :return: A tuple (`Translator`, `Language`, `Person`), ordered
> - by language name in English.
> + Prefetches display-related properties.
> +
> + :return: A result set of (`Translator`, `Language`, `Person`),
> + ordered by language name in English.
> + """
> +
> + def fetchProjectsFo
I would ask you to use "fetchProductsF orDisplay" as l...