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 | 17 | ForeignKey, | 17 | ForeignKey, |
6 | 18 | StringCol, | 18 | StringCol, |
7 | 19 | ) | 19 | ) |
13 | 20 | from storm.expr import ( | 20 | from storm.expr import LeftJoin |
9 | 21 | LeftJoin, | ||
10 | 22 | NamedFunc, | ||
11 | 23 | Select, | ||
12 | 24 | ) | ||
14 | 25 | from storm.info import ClassAlias | 21 | from storm.info import ClassAlias |
15 | 26 | from storm.locals import SQL | 22 | from storm.locals import SQL |
16 | 27 | from storm.store import Store | 23 | from storm.store import Store |
17 | @@ -51,11 +47,9 @@ | |||
18 | 51 | from lp.registry.interfaces.product import ( | 47 | from lp.registry.interfaces.product import ( |
19 | 52 | IProduct, | 48 | IProduct, |
20 | 53 | IProductSet, | 49 | IProductSet, |
21 | 54 | License, | ||
22 | 55 | ) | 50 | ) |
23 | 56 | from lp.registry.interfaces.projectgroup import IProjectGroupSet | 51 | from lp.registry.interfaces.projectgroup import IProjectGroupSet |
24 | 57 | from lp.registry.model.featuredproject import FeaturedProject | 52 | from lp.registry.model.featuredproject import FeaturedProject |
25 | 58 | from lp.registry.model.productlicense import ProductLicense | ||
26 | 59 | 53 | ||
27 | 60 | 54 | ||
28 | 61 | __all__ = [ | 55 | __all__ = [ |
29 | @@ -197,23 +191,15 @@ | |||
30 | 197 | 191 | ||
31 | 198 | def search(self, text, limit): | 192 | def search(self, text, limit): |
32 | 199 | """See `IPillarSet`.""" | 193 | """See `IPillarSet`.""" |
36 | 200 | # These classes are imported in this method to prevent an import loop. | 194 | # Avoid circular import. |
37 | 201 | from lp.registry.model.product import ( | 195 | from lp.registry.model.product import ProductWithLicenses |
35 | 202 | Product, ProductWithLicenses) | ||
38 | 203 | if limit is None: | 196 | if limit is None: |
39 | 204 | limit = config.launchpad.default_batch_size | 197 | limit = config.launchpad.default_batch_size |
40 | 205 | 198 | ||
41 | 206 | class Array(NamedFunc): | ||
42 | 207 | """Storm representation of the array() PostgreSQL function.""" | ||
43 | 208 | name = 'array' | ||
44 | 209 | |||
45 | 210 | # Pull out the licenses as a subselect which is converted | 199 | # Pull out the licenses as a subselect which is converted |
46 | 211 | # into a PostgreSQL array so that multiple licenses per product | 200 | # into a PostgreSQL array so that multiple licenses per product |
47 | 212 | # can be retrieved in a single row for each product. | 201 | # can be retrieved in a single row for each product. |
52 | 213 | extra_column = Array( | 202 | extra_column = ProductWithLicenses.composeLicensesColumn() |
49 | 214 | Select(columns=[ProductLicense.license], | ||
50 | 215 | where=(ProductLicense.product == Product.id), | ||
51 | 216 | tables=[ProductLicense])) | ||
53 | 217 | result = self.build_search_query(text, [extra_column]) | 203 | result = self.build_search_query(text, [extra_column]) |
54 | 218 | 204 | ||
55 | 219 | # If the search text matches the name or title of the | 205 | # If the search text matches the name or title of the |
56 | @@ -244,18 +230,12 @@ | |||
57 | 244 | stacklevel=2) | 230 | stacklevel=2) |
58 | 245 | pillars = [] | 231 | pillars = [] |
59 | 246 | # Prefill pillar.product.licenses. | 232 | # Prefill pillar.product.licenses. |
61 | 247 | for pillar_name, other, product, project, distro, license_ids in ( | 233 | for pillar_name, other, product, project, distro, licenses in ( |
62 | 248 | result[:limit]): | 234 | result[:limit]): |
63 | 249 | pillar = pillar_name.pillar | 235 | pillar = pillar_name.pillar |
64 | 250 | if IProduct.providedBy(pillar): | 236 | if IProduct.providedBy(pillar): |
73 | 251 | licenses = [ | 237 | pillar = ProductWithLicenses(pillar, licenses) |
74 | 252 | License.items[license_id] | 238 | pillars.append(pillar) |
67 | 253 | for license_id in license_ids] | ||
68 | 254 | product_with_licenses = ProductWithLicenses( | ||
69 | 255 | pillar, tuple(sorted(licenses))) | ||
70 | 256 | pillars.append(product_with_licenses) | ||
71 | 257 | else: | ||
72 | 258 | pillars.append(pillar) | ||
75 | 259 | return pillars | 239 | return pillars |
76 | 260 | 240 | ||
77 | 261 | def add_featured_project(self, project): | 241 | def add_featured_project(self, project): |
78 | 262 | 242 | ||
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 @@ | |||
84 | 1 | # Copyright 2009 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2009-2010 Canonical Ltd. This software is licensed under the |
85 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
86 | 3 | # pylint: disable-msg=E0611,W0212 | 3 | # pylint: disable-msg=E0611,W0212 |
87 | 4 | 4 | ||
88 | @@ -25,11 +25,13 @@ | |||
89 | 25 | SQLObjectNotFound, | 25 | SQLObjectNotFound, |
90 | 26 | StringCol, | 26 | StringCol, |
91 | 27 | ) | 27 | ) |
92 | 28 | from storm.expr import NamedFunc | ||
93 | 28 | from storm.locals import ( | 29 | from storm.locals import ( |
94 | 29 | And, | 30 | And, |
95 | 30 | Desc, | 31 | Desc, |
96 | 31 | Int, | 32 | Int, |
97 | 32 | Join, | 33 | Join, |
98 | 34 | Select, | ||
99 | 33 | SQL, | 35 | SQL, |
100 | 34 | Store, | 36 | Store, |
101 | 35 | Unicode, | 37 | Unicode, |
102 | @@ -85,7 +87,6 @@ | |||
103 | 85 | SpecificationDefinitionStatus, | 87 | SpecificationDefinitionStatus, |
104 | 86 | SpecificationFilter, | 88 | SpecificationFilter, |
105 | 87 | SpecificationImplementationStatus, | 89 | SpecificationImplementationStatus, |
106 | 88 | SpecificationSort, | ||
107 | 89 | ) | 90 | ) |
108 | 90 | from lp.blueprints.model.specification import ( | 91 | from lp.blueprints.model.specification import ( |
109 | 91 | HasSpecificationsMixin, | 92 | HasSpecificationsMixin, |
110 | @@ -201,14 +202,25 @@ | |||
111 | 201 | return LicenseStatus.OPEN_SOURCE | 202 | return LicenseStatus.OPEN_SOURCE |
112 | 202 | 203 | ||
113 | 203 | 204 | ||
114 | 205 | class Array(NamedFunc): | ||
115 | 206 | """Implements the postgres "array" function in Storm.""" | ||
116 | 207 | name = 'array' | ||
117 | 208 | |||
118 | 209 | |||
119 | 204 | class ProductWithLicenses: | 210 | class ProductWithLicenses: |
120 | 205 | """Caches `Product.licenses`.""" | 211 | """Caches `Product.licenses`.""" |
121 | 206 | 212 | ||
122 | 207 | delegates(IProduct, 'product') | 213 | delegates(IProduct, 'product') |
123 | 208 | 214 | ||
125 | 209 | def __init__(self, product, licenses): | 215 | def __init__(self, product, license_ids): |
126 | 216 | """Initialize a `ProductWithLicenses`. | ||
127 | 217 | |||
128 | 218 | :param product: the `Product` to wrap. | ||
129 | 219 | :param license_ids: a sequence of numeric `License` ids. | ||
130 | 220 | """ | ||
131 | 210 | self.product = product | 221 | self.product = product |
133 | 211 | self._licenses = licenses | 222 | self._licenses = tuple([ |
134 | 223 | License.items[id] for id in sorted(license_ids)]) | ||
135 | 212 | 224 | ||
136 | 213 | @property | 225 | @property |
137 | 214 | def licenses(self): | 226 | def licenses(self): |
138 | @@ -223,8 +235,40 @@ | |||
139 | 223 | `Product.licenses`, which is not cached, instead of | 235 | `Product.licenses`, which is not cached, instead of |
140 | 224 | `ProductWithLicenses.licenses`, which is cached. | 236 | `ProductWithLicenses.licenses`, which is cached. |
141 | 225 | """ | 237 | """ |
142 | 238 | naked_product = removeSecurityProxy(self.product) | ||
143 | 226 | return get_license_status( | 239 | return get_license_status( |
145 | 227 | self.license_approved, self.license_reviewed, self.licenses) | 240 | naked_product.license_approved, naked_product.license_reviewed, |
146 | 241 | self.licenses) | ||
147 | 242 | |||
148 | 243 | @classmethod | ||
149 | 244 | def composeLicensesColumn(cls, for_class=None): | ||
150 | 245 | """Compose a Storm column specification for licenses. | ||
151 | 246 | |||
152 | 247 | Use this to render a list of `Product` linkes without querying | ||
153 | 248 | licenses for each one individually. | ||
154 | 249 | |||
155 | 250 | It lets you prefetch the licensing information in the same | ||
156 | 251 | query that fetches a `Product`. Just add the column spec | ||
157 | 252 | returned by this function to the query, and pass it to the | ||
158 | 253 | `ProductWithLicenses` constructor: | ||
159 | 254 | |||
160 | 255 | license_column = ProductWithLicenses.composeLicensesColumn() | ||
161 | 256 | products_with_licenses = [ | ||
162 | 257 | ProductWithLicenses(product, licenses) | ||
163 | 258 | for product, licenses in store.find(Product, license_column) | ||
164 | 259 | ] | ||
165 | 260 | |||
166 | 261 | :param for_class: Class to find licenses for. Defaults to | ||
167 | 262 | `Product`, but could also be a Storm `ClassAlias`. | ||
168 | 263 | """ | ||
169 | 264 | if for_class is None: | ||
170 | 265 | for_class = Product | ||
171 | 266 | |||
172 | 267 | return Array( | ||
173 | 268 | Select( | ||
174 | 269 | columns=[ProductLicense.license], | ||
175 | 270 | where=(ProductLicense.product == for_class.id), | ||
176 | 271 | tables=[ProductLicense])) | ||
177 | 228 | 272 | ||
178 | 229 | 273 | ||
179 | 230 | class Product(SQLBase, BugTargetBase, MakesAnnouncements, | 274 | class Product(SQLBase, BugTargetBase, MakesAnnouncements, |
180 | 231 | 275 | ||
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 | 1 | # Copyright 2010 Canonical Ltd. This software is licensed under the | ||
186 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
187 | 3 | |||
188 | 4 | """Unit tests for `ProductWithLicenses`.""" | ||
189 | 5 | |||
190 | 6 | __metaclass__ = type | ||
191 | 7 | |||
192 | 8 | from operator import attrgetter | ||
193 | 9 | |||
194 | 10 | from storm.store import Store | ||
195 | 11 | from zope.interface.verify import verifyObject | ||
196 | 12 | |||
197 | 13 | from canonical.launchpad.ftests import ( | ||
198 | 14 | ANONYMOUS, | ||
199 | 15 | login, | ||
200 | 16 | ) | ||
201 | 17 | from canonical.testing import DatabaseFunctionalLayer | ||
202 | 18 | from lp.registry.interfaces.product import ( | ||
203 | 19 | IProduct, | ||
204 | 20 | License, | ||
205 | 21 | LicenseStatus, | ||
206 | 22 | ) | ||
207 | 23 | from lp.registry.model.product import ( | ||
208 | 24 | Product, | ||
209 | 25 | ProductWithLicenses, | ||
210 | 26 | ) | ||
211 | 27 | from lp.testing import TestCaseWithFactory | ||
212 | 28 | |||
213 | 29 | |||
214 | 30 | class TestProductWithLicenses(TestCaseWithFactory): | ||
215 | 31 | |||
216 | 32 | layer = DatabaseFunctionalLayer | ||
217 | 33 | |||
218 | 34 | def test_baseline(self): | ||
219 | 35 | product = self.factory.makeProduct() | ||
220 | 36 | product_with_licenses = ProductWithLicenses(product, []) | ||
221 | 37 | # Log in--a full verification takes Edit privileges. | ||
222 | 38 | login('foo.bar@canonical.com') | ||
223 | 39 | self.assertTrue(verifyObject(IProduct, product_with_licenses)) | ||
224 | 40 | |||
225 | 41 | def test_uses_cached_licenses(self): | ||
226 | 42 | # The ProductWithLicenses' licensing information is based purely | ||
227 | 43 | # on the cached licenses list. The database is not queried to | ||
228 | 44 | # determine licensing status. | ||
229 | 45 | product = self.factory.makeProduct(licenses=[License.BSD]) | ||
230 | 46 | product_with_licenses = ProductWithLicenses( | ||
231 | 47 | product, [License.OTHER_PROPRIETARY.value]) | ||
232 | 48 | license_status = product_with_licenses.license_status | ||
233 | 49 | self.assertEqual(LicenseStatus.PROPRIETARY, license_status) | ||
234 | 50 | |||
235 | 51 | def test_sorts_licenses(self): | ||
236 | 52 | # The ProductWithLicenses constructor sorts the Licenses by | ||
237 | 53 | # numeric value. | ||
238 | 54 | product = self.factory.makeProduct() | ||
239 | 55 | licenses = [License.AFFERO, License.BSD, License.MIT] | ||
240 | 56 | |||
241 | 57 | # Feed the constructor a list of ids in the wrong order. | ||
242 | 58 | product_with_licenses = ProductWithLicenses( | ||
243 | 59 | product, | ||
244 | 60 | sorted([license.value for license in licenses], reverse=True)) | ||
245 | 61 | |||
246 | 62 | expected = sorted(licenses, key=attrgetter('value')) | ||
247 | 63 | self.assertEqual(tuple(expected), product_with_licenses.licenses) | ||
248 | 64 | |||
249 | 65 | def test_compose_column_without_licenses_produces_empty(self): | ||
250 | 66 | # The licenses column that ProductWithLicenses produces for a | ||
251 | 67 | # product without licenses contains an empty list. | ||
252 | 68 | product = self.factory.makeProduct(licenses=[]) | ||
253 | 69 | column = ProductWithLicenses.composeLicensesColumn() | ||
254 | 70 | store = Store.of(product) | ||
255 | 71 | result = list(store.find((Product, column), Product.id == product.id)) | ||
256 | 72 | |||
257 | 73 | self.assertEqual([(product, [])], result) | ||
258 | 74 | |||
259 | 75 | def test_licenses_column_contains_licensing_info(self): | ||
260 | 76 | # Feeding the licenses column into the ProductWithLicenses | ||
261 | 77 | # constructor seeds it with the appropriate licenses. | ||
262 | 78 | product = self.factory.makeProduct( | ||
263 | 79 | licenses=[License.OTHER_PROPRIETARY]) | ||
264 | 80 | column = ProductWithLicenses.composeLicensesColumn() | ||
265 | 81 | store = Store.of(product) | ||
266 | 82 | row = store.find((Product, column), Product.id == product.id).one() | ||
267 | 83 | |||
268 | 84 | product_with_licenses = ProductWithLicenses(*row) | ||
269 | 85 | licenses = product_with_licenses.licenses | ||
270 | 86 | license_status = product_with_licenses.license_status | ||
271 | 87 | self.assertEqual((License.OTHER_PROPRIETARY, ), licenses) | ||
272 | 88 | self.assertEqual(LicenseStatus.PROPRIETARY, license_status) | ||
273 | 89 | |||
274 | 90 | def test_licenses_column_aggregates(self): | ||
275 | 91 | # Adding a licensing column for a product with multiple licenses | ||
276 | 92 | # still finds a single product, not one per license. | ||
277 | 93 | licenses = [License.AFFERO, License.GNU_GPL_V3] | ||
278 | 94 | product = self.factory.makeProduct(licenses=licenses) | ||
279 | 95 | column = ProductWithLicenses.composeLicensesColumn() | ||
280 | 96 | store = Store.of(product) | ||
281 | 97 | result = list(store.find((Product, column), Product.id == product.id)) | ||
282 | 98 | |||
283 | 99 | self.assertEqual(1, len(result)) | ||
284 | 100 | found_product, found_licenses = result[0] | ||
285 | 101 | self.assertEqual(product, found_product) | ||
286 | 102 | self.assertEqual(len(licenses), len(found_licenses)) | ||
287 | 103 | |||
288 | 104 | def test_license_status_is_public(self): | ||
289 | 105 | # The license_status attribute can be read by anyone, on | ||
290 | 106 | # ProductWithLicenses as on Product. | ||
291 | 107 | product = self.factory.makeProduct(licenses=[License.BSD]) | ||
292 | 108 | product_with_licenses = ProductWithLicenses( | ||
293 | 109 | product, [License.BSD.value]) | ||
294 | 110 | login(ANONYMOUS) | ||
295 | 111 | self.assertEqual( | ||
296 | 112 | LicenseStatus.OPEN_SOURCE, product.license_status) | ||
297 | 113 | self.assertEqual( | ||
298 | 114 | LicenseStatus.OPEN_SOURCE, product_with_licenses.license_status) | ||
299 | 0 | 115 | ||
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 | 5 | 5 | ||
305 | 6 | __metaclass__ = type | 6 | __metaclass__ = type |
306 | 7 | 7 | ||
309 | 8 | import unittest | 8 | import transaction |
308 | 9 | |||
310 | 10 | from zope.component import getUtility | 9 | from zope.component import getUtility |
311 | 11 | 10 | ||
312 | 12 | from canonical.launchpad.webapp.servers import LaunchpadTestRequest | 11 | from canonical.launchpad.webapp.servers import LaunchpadTestRequest |
313 | @@ -40,6 +39,7 @@ | |||
314 | 40 | # translator_list composes dicts using _makeTranslatorDict. | 39 | # translator_list composes dicts using _makeTranslatorDict. |
315 | 41 | group = self._makeGroup() | 40 | group = self._makeGroup() |
316 | 42 | tr_translator = self.factory.makeTranslator('tr', group) | 41 | tr_translator = self.factory.makeTranslator('tr', group) |
317 | 42 | transaction.commit() | ||
318 | 43 | view = self._makeView(group) | 43 | view = self._makeView(group) |
319 | 44 | translator_dict = view._makeTranslatorDict( | 44 | translator_dict = view._makeTranslatorDict( |
320 | 45 | tr_translator, tr_translator.language, tr_translator.translator) | 45 | tr_translator, tr_translator.language, tr_translator.translator) |
321 | @@ -62,7 +62,3 @@ | |||
322 | 62 | self.assertEqual(xhosa.datecreated, output['datecreated']) | 62 | self.assertEqual(xhosa.datecreated, output['datecreated']) |
323 | 63 | self.assertEqual(xhosa.style_guide_url, output['style_guide_url']) | 63 | self.assertEqual(xhosa.style_guide_url, output['style_guide_url']) |
324 | 64 | self.assertEqual(xhosa, output['context']) | 64 | self.assertEqual(xhosa, output['context']) |
325 | 65 | |||
326 | 66 | |||
327 | 67 | def test_suite(): | ||
328 | 68 | return unittest.TestLoader().loadTestsFromName(__name__) | ||
329 | 69 | 65 | ||
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 @@ | |||
335 | 1 | # Copyright 2009 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2009-2010 Canonical Ltd. This software is licensed under the |
336 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
337 | 3 | 3 | ||
338 | 4 | """Browser code for translation groups.""" | 4 | """Browser code for translation groups.""" |
339 | @@ -26,6 +26,7 @@ | |||
340 | 26 | LaunchpadFormView, | 26 | LaunchpadFormView, |
341 | 27 | LaunchpadView, | 27 | LaunchpadView, |
342 | 28 | ) | 28 | ) |
343 | 29 | from canonical.launchpad.webapp.authorization import check_permission | ||
344 | 29 | from canonical.launchpad.webapp.breadcrumb import Breadcrumb | 30 | from canonical.launchpad.webapp.breadcrumb import Breadcrumb |
345 | 30 | from lp.app.errors import NotFoundError | 31 | from lp.app.errors import NotFoundError |
346 | 31 | from lp.registry.browser.objectreassignment import ObjectReassignmentView | 32 | from lp.registry.browser.objectreassignment import ObjectReassignmentView |
347 | @@ -67,6 +68,7 @@ | |||
348 | 67 | self.context = context | 68 | self.context = context |
349 | 68 | self.request = request | 69 | self.request = request |
350 | 69 | self.translation_groups = getUtility(ITranslationGroupSet) | 70 | self.translation_groups = getUtility(ITranslationGroupSet) |
351 | 71 | self.user_can_edit = check_permission('launchpad.Edit', self.context) | ||
352 | 70 | 72 | ||
353 | 71 | @property | 73 | @property |
354 | 72 | def label(self): | 74 | def label(self): |
355 | @@ -96,8 +98,7 @@ | |||
356 | 96 | """List of dicts describing the translation teams.""" | 98 | """List of dicts describing the translation teams.""" |
357 | 97 | return [ | 99 | return [ |
358 | 98 | self._makeTranslatorDict(*data) | 100 | self._makeTranslatorDict(*data) |
361 | 99 | for data in self.context.fetchTranslatorData() | 101 | for data in self.context.fetchTranslatorData()] |
360 | 100 | ] | ||
362 | 101 | 102 | ||
363 | 102 | 103 | ||
364 | 103 | class TranslationGroupAddTranslatorView(LaunchpadFormView): | 104 | class TranslationGroupAddTranslatorView(LaunchpadFormView): |
365 | 104 | 105 | ||
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 @@ | |||
371 | 1 | = Translation Groups = | 1 | Translation Groups |
372 | 2 | ================== | ||
373 | 2 | 3 | ||
374 | 3 | >>> from zope.component import getUtility | 4 | >>> from zope.component import getUtility |
375 | 4 | 5 | ||
376 | @@ -63,12 +64,12 @@ | |||
377 | 63 | 64 | ||
378 | 64 | No Privileges Person isn't allowed to translate into Welsh. | 65 | No Privileges Person isn't allowed to translate into Welsh. |
379 | 65 | 66 | ||
386 | 66 | >>> series = evolution.getSeries('trunk') | 67 | >>> series = evolution.getSeries('trunk') |
387 | 67 | >>> subset = potemplate_set.getSubset(productseries=series) | 68 | >>> subset = potemplate_set.getSubset(productseries=series) |
388 | 68 | >>> potemplate = subset['evolution-2.2'] | 69 | >>> potemplate = subset['evolution-2.2'] |
389 | 69 | >>> pofile = potemplate.newPOFile('cy') | 70 | >>> pofile = potemplate.newPOFile('cy') |
390 | 70 | >>> pofile.canEditTranslations(no_priv) | 71 | >>> pofile.canEditTranslations(no_priv) |
391 | 71 | False | 72 | False |
392 | 72 | 73 | ||
393 | 73 | Let's add him to the group. | 74 | Let's add him to the group. |
394 | 74 | 75 | ||
395 | @@ -176,6 +177,7 @@ | |||
396 | 176 | >>> de_translator = factory.makeTranslator('de', group, person=de_team) | 177 | >>> de_translator = factory.makeTranslator('de', group, person=de_team) |
397 | 177 | >>> nl_translator = factory.makeTranslator('nl', group, person=nl_team) | 178 | >>> nl_translator = factory.makeTranslator('nl', group, person=nl_team) |
398 | 178 | >>> la_translator = factory.makeTranslator('la', group, person=la_team) | 179 | >>> la_translator = factory.makeTranslator('la', group, person=la_team) |
399 | 180 | >>> transaction.commit() | ||
400 | 179 | 181 | ||
401 | 180 | The method returns tuples of respectively a Translator ("translation | 182 | The method returns tuples of respectively a Translator ("translation |
402 | 181 | group membership entry"), its language, and the actual team or person | 183 | group membership entry"), its language, and the actual team or person |
403 | 182 | 184 | ||
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 | 186 | def fetchTranslatorData(): | 186 | def fetchTranslatorData(): |
409 | 187 | """Fetch translators and related data. | 187 | """Fetch translators and related data. |
410 | 188 | 188 | ||
413 | 189 | :return: A tuple (`Translator`, `Language`, `Person`), ordered | 189 | Prefetches display-related properties. |
414 | 190 | by language name in English. | 190 | |
415 | 191 | :return: A result set of (`Translator`, `Language`, `Person`), | ||
416 | 192 | ordered by language name in English. | ||
417 | 193 | """ | ||
418 | 194 | |||
419 | 195 | def fetchProjectsForDisplay(): | ||
420 | 196 | """Fetch `Product`s using this group, for display purposes. | ||
421 | 197 | |||
422 | 198 | Prefetches display-related properties. | ||
423 | 199 | |||
424 | 200 | :return: A result set of `Product`, ordered by display name. | ||
425 | 201 | """ | ||
426 | 202 | |||
427 | 203 | def fetchProjectGroupsForDisplay(): | ||
428 | 204 | """Fetch `Project`s using this group, for display purposes. | ||
429 | 205 | |||
430 | 206 | Prefetches display-related properties. | ||
431 | 207 | |||
432 | 208 | :return: A result set of `Project`, ordered by display name. | ||
433 | 209 | """ | ||
434 | 210 | |||
435 | 211 | def fetchDistrosForDisplay(): | ||
436 | 212 | """Fetch `Distribution`s using this group, for display purposes. | ||
437 | 213 | |||
438 | 214 | Prefetches display-related properties. | ||
439 | 215 | |||
440 | 216 | :return: A result set of `Distribution`, ordered by display name. | ||
441 | 191 | """ | 217 | """ |
442 | 192 | 218 | ||
443 | 193 | 219 | ||
444 | 194 | 220 | ||
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 @@ | |||
450 | 1 | # Copyright 2009 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2009-2010 Canonical Ltd. This software is licensed under the |
451 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
452 | 3 | 3 | ||
453 | 4 | # pylint: disable-msg=E0611,W0212 | 4 | # pylint: disable-msg=E0611,W0212 |
454 | @@ -6,7 +6,7 @@ | |||
455 | 6 | __metaclass__ = type | 6 | __metaclass__ = type |
456 | 7 | __all__ = [ | 7 | __all__ = [ |
457 | 8 | 'TranslationGroup', | 8 | 'TranslationGroup', |
459 | 9 | 'TranslationGroupSet' | 9 | 'TranslationGroupSet', |
460 | 10 | ] | 10 | ] |
461 | 11 | 11 | ||
462 | 12 | from sqlobject import ( | 12 | from sqlobject import ( |
463 | @@ -16,25 +16,32 @@ | |||
464 | 16 | SQLRelatedJoin, | 16 | SQLRelatedJoin, |
465 | 17 | StringCol, | 17 | StringCol, |
466 | 18 | ) | 18 | ) |
468 | 19 | from storm.expr import Join | 19 | from storm.expr import ( |
469 | 20 | Join, | ||
470 | 21 | LeftJoin, | ||
471 | 22 | ) | ||
472 | 20 | from storm.store import Store | 23 | from storm.store import Store |
473 | 21 | from zope.component import getUtility | ||
474 | 22 | from zope.interface import implements | 24 | from zope.interface import implements |
475 | 23 | 25 | ||
476 | 24 | from canonical.database.constants import DEFAULT | 26 | from canonical.database.constants import DEFAULT |
477 | 25 | from canonical.database.datetimecol import UtcDateTimeCol | 27 | from canonical.database.datetimecol import UtcDateTimeCol |
478 | 26 | from canonical.database.sqlbase import SQLBase | 28 | from canonical.database.sqlbase import SQLBase |
483 | 27 | from canonical.launchpad.webapp.interfaces import ( | 29 | from canonical.launchpad.database.librarian import ( |
484 | 28 | DEFAULT_FLAVOR, | 30 | LibraryFileAlias, |
485 | 29 | IStoreSelector, | 31 | LibraryFileContent, |
482 | 30 | MAIN_STORE, | ||
486 | 31 | ) | 32 | ) |
487 | 33 | from canonical.launchpad.interfaces.lpstorm import ISlaveStore | ||
488 | 32 | from lp.app.errors import NotFoundError | 34 | from lp.app.errors import NotFoundError |
489 | 33 | from lp.registry.interfaces.person import validate_public_person | 35 | from lp.registry.interfaces.person import validate_public_person |
490 | 36 | from lp.registry.model.distribution import Distribution | ||
491 | 34 | from lp.registry.model.person import Person | 37 | from lp.registry.model.person import Person |
493 | 35 | from lp.registry.model.product import Product | 38 | from lp.registry.model.product import ( |
494 | 39 | Product, | ||
495 | 40 | ProductWithLicenses, | ||
496 | 41 | ) | ||
497 | 36 | from lp.registry.model.projectgroup import ProjectGroup | 42 | from lp.registry.model.projectgroup import ProjectGroup |
498 | 37 | from lp.registry.model.teammembership import TeamParticipation | 43 | from lp.registry.model.teammembership import TeamParticipation |
499 | 44 | from lp.services.database.prejoin import prejoin | ||
500 | 38 | from lp.services.worlddata.model.language import Language | 45 | from lp.services.worlddata.model.language import Language |
501 | 39 | from lp.translations.interfaces.translationgroup import ( | 46 | from lp.translations.interfaces.translationgroup import ( |
502 | 40 | ITranslationGroup, | 47 | ITranslationGroup, |
503 | @@ -71,16 +78,15 @@ | |||
504 | 71 | 78 | ||
505 | 72 | def __getitem__(self, language_code): | 79 | def __getitem__(self, language_code): |
506 | 73 | """See `ITranslationGroup`.""" | 80 | """See `ITranslationGroup`.""" |
509 | 74 | store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR) | 81 | query = Store.of(self).find( |
508 | 75 | query = store.find( | ||
510 | 76 | Translator, | 82 | Translator, |
511 | 77 | Translator.translationgroup == self, | 83 | Translator.translationgroup == self, |
512 | 78 | Translator.languageID == Language.id, | 84 | Translator.languageID == Language.id, |
513 | 79 | Language.code == language_code) | 85 | Language.code == language_code) |
515 | 80 | 86 | ||
516 | 81 | translator = query.one() | 87 | translator = query.one() |
517 | 82 | if translator is None: | 88 | if translator is None: |
519 | 83 | raise NotFoundError, language_code | 89 | raise NotFoundError(language_code) |
520 | 84 | 90 | ||
521 | 85 | return translator | 91 | return translator |
522 | 86 | 92 | ||
523 | @@ -141,14 +147,102 @@ | |||
524 | 141 | return 0 | 147 | return 0 |
525 | 142 | 148 | ||
526 | 143 | def fetchTranslatorData(self): | 149 | def fetchTranslatorData(self): |
531 | 144 | """See ITranslationGroup.""" | 150 | """See `ITranslationGroup`.""" |
532 | 145 | store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR) | 151 | # Fetch Translator, Language, and Person; but also prefetch the |
533 | 146 | translator_data = store.find( | 152 | # icon information. |
534 | 147 | (Translator, Language, Person), | 153 | using = [ |
535 | 154 | Translator, | ||
536 | 155 | Language, | ||
537 | 156 | Person, | ||
538 | 157 | LeftJoin(LibraryFileAlias, LibraryFileAlias.id == Person.iconID), | ||
539 | 158 | LeftJoin( | ||
540 | 159 | LibraryFileContent, | ||
541 | 160 | LibraryFileContent.id == LibraryFileAlias.contentID), | ||
542 | 161 | ] | ||
543 | 162 | tables = ( | ||
544 | 163 | Translator, | ||
545 | 164 | Language, | ||
546 | 165 | Person, | ||
547 | 166 | LibraryFileAlias, | ||
548 | 167 | LibraryFileContent, | ||
549 | 168 | ) | ||
550 | 169 | translator_data = Store.of(self).using(*using).find( | ||
551 | 170 | tables, | ||
552 | 148 | Translator.translationgroup == self, | 171 | Translator.translationgroup == self, |
553 | 149 | Language.id == Translator.languageID, | 172 | Language.id == Translator.languageID, |
554 | 150 | Person.id == Translator.translatorID) | 173 | Person.id == Translator.translatorID) |
556 | 151 | return translator_data.order_by(Language.englishname) | 174 | |
557 | 175 | return prejoin( | ||
558 | 176 | translator_data.order_by(Language.englishname), | ||
559 | 177 | slice(0, 3)) | ||
560 | 178 | |||
561 | 179 | def fetchProjectsForDisplay(self): | ||
562 | 180 | """See `ITranslationGroup`.""" | ||
563 | 181 | using = [ | ||
564 | 182 | Product, | ||
565 | 183 | LeftJoin(LibraryFileAlias, LibraryFileAlias.id == Product.iconID), | ||
566 | 184 | LeftJoin( | ||
567 | 185 | LibraryFileContent, | ||
568 | 186 | LibraryFileContent.id == LibraryFileAlias.contentID), | ||
569 | 187 | ] | ||
570 | 188 | columns = ( | ||
571 | 189 | Product, | ||
572 | 190 | ProductWithLicenses.composeLicensesColumn(), | ||
573 | 191 | LibraryFileAlias, | ||
574 | 192 | LibraryFileContent, | ||
575 | 193 | ) | ||
576 | 194 | product_data = ISlaveStore(Product).using(*using).find( | ||
577 | 195 | columns, | ||
578 | 196 | Product.translationgroupID == self.id, Product.active == True) | ||
579 | 197 | product_data = product_data.order_by(Product.displayname) | ||
580 | 198 | |||
581 | 199 | return [ | ||
582 | 200 | ProductWithLicenses(product, tuple(licenses)) | ||
583 | 201 | for product, licenses, icon_alias, icon_content in product_data] | ||
584 | 202 | |||
585 | 203 | def fetchProjectGroupsForDisplay(self): | ||
586 | 204 | """See `ITranslationGroup`.""" | ||
587 | 205 | using = [ | ||
588 | 206 | ProjectGroup, | ||
589 | 207 | LeftJoin( | ||
590 | 208 | LibraryFileAlias, LibraryFileAlias.id == ProjectGroup.iconID), | ||
591 | 209 | LeftJoin( | ||
592 | 210 | LibraryFileContent, | ||
593 | 211 | LibraryFileContent.id == LibraryFileAlias.contentID), | ||
594 | 212 | ] | ||
595 | 213 | tables = ( | ||
596 | 214 | ProjectGroup, | ||
597 | 215 | LibraryFileAlias, | ||
598 | 216 | LibraryFileContent, | ||
599 | 217 | ) | ||
600 | 218 | project_data = ISlaveStore(ProjectGroup).using(*using).find( | ||
601 | 219 | tables, | ||
602 | 220 | ProjectGroup.translationgroupID == self.id, | ||
603 | 221 | ProjectGroup.active == True) | ||
604 | 222 | |||
605 | 223 | return prejoin( | ||
606 | 224 | project_data.order_by(ProjectGroup.displayname), slice(0, 1)) | ||
607 | 225 | |||
608 | 226 | def fetchDistrosForDisplay(self): | ||
609 | 227 | """See `ITranslationGroup`.""" | ||
610 | 228 | using = [ | ||
611 | 229 | Distribution, | ||
612 | 230 | LeftJoin( | ||
613 | 231 | LibraryFileAlias, LibraryFileAlias.id == Distribution.iconID), | ||
614 | 232 | LeftJoin( | ||
615 | 233 | LibraryFileContent, | ||
616 | 234 | LibraryFileContent.id == LibraryFileAlias.contentID), | ||
617 | 235 | ] | ||
618 | 236 | tables = ( | ||
619 | 237 | Distribution, | ||
620 | 238 | LibraryFileAlias, | ||
621 | 239 | LibraryFileContent, | ||
622 | 240 | ) | ||
623 | 241 | distro_data = ISlaveStore(Distribution).using(*using).find( | ||
624 | 242 | tables, Distribution.translationgroupID == self.id) | ||
625 | 243 | |||
626 | 244 | return prejoin( | ||
627 | 245 | distro_data.order_by(Distribution.displayname), slice(0, 1)) | ||
628 | 152 | 246 | ||
629 | 153 | 247 | ||
630 | 154 | class TranslationGroupSet: | 248 | class TranslationGroupSet: |
631 | @@ -174,7 +268,7 @@ | |||
632 | 174 | try: | 268 | try: |
633 | 175 | return TranslationGroup.byName(name) | 269 | return TranslationGroup.byName(name) |
634 | 176 | except SQLObjectNotFound: | 270 | except SQLObjectNotFound: |
636 | 177 | raise NotFoundError, name | 271 | raise NotFoundError(name) |
637 | 178 | 272 | ||
638 | 179 | def new(self, name, title, summary, translation_guide_url, owner): | 273 | def new(self, name, title, summary, translation_guide_url, owner): |
639 | 180 | """See ITranslationGroupSet.""" | 274 | """See ITranslationGroupSet.""" |
640 | @@ -193,7 +287,7 @@ | |||
641 | 193 | Join(Translator, | 287 | Join(Translator, |
642 | 194 | Translator.translationgroupID == TranslationGroup.id), | 288 | Translator.translationgroupID == TranslationGroup.id), |
643 | 195 | Join(TeamParticipation, | 289 | Join(TeamParticipation, |
645 | 196 | TeamParticipation.teamID == Translator.translatorID) | 290 | TeamParticipation.teamID == Translator.translatorID), |
646 | 197 | ] | 291 | ] |
647 | 198 | result = store.using(*origin).find( | 292 | result = store.using(*origin).find( |
648 | 199 | TranslationGroup, TeamParticipation.person == person) | 293 | TranslationGroup, TeamParticipation.person == person) |
649 | @@ -203,4 +297,3 @@ | |||
650 | 203 | def getGroupsCount(self): | 297 | def getGroupsCount(self): |
651 | 204 | """See ITranslationGroupSet.""" | 298 | """See ITranslationGroupSet.""" |
652 | 205 | return TranslationGroup.select().count() | 299 | return TranslationGroup.select().count() |
653 | 206 | |||
654 | 207 | 300 | ||
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 | 13 | <a tal:content="context/owner/fmt:displayname" | 13 | <a tal:content="context/owner/fmt:displayname" |
660 | 14 | tal:attributes="href context/owner/fmt:url" | 14 | tal:attributes="href context/owner/fmt:url" |
661 | 15 | class="link" /> | 15 | class="link" /> |
663 | 16 | <a tal:condition="context/required:launchpad.Edit" | 16 | <a tal:condition="view/user_can_edit" |
664 | 17 | href="+reassign" title="Change administrator" | 17 | href="+reassign" title="Change administrator" |
665 | 18 | class="edit sprite" id="link-reassign"></a> | 18 | class="edit sprite" id="link-reassign"></a> |
666 | 19 | </p> | 19 | </p> |
667 | @@ -32,7 +32,7 @@ | |||
668 | 32 | translations in this translation group.</em> | 32 | translations in this translation group.</em> |
669 | 33 | </p> | 33 | </p> |
670 | 34 | 34 | ||
672 | 35 | <div tal:condition="context/required:launchpad.Edit"> | 35 | <div tal:condition="view/user_can_edit"> |
673 | 36 | <a href="+edit" class="edit sprite">Change details</a> | 36 | <a href="+edit" class="edit sprite">Change details</a> |
674 | 37 | </div> | 37 | </div> |
675 | 38 | 38 | ||
676 | @@ -40,7 +40,8 @@ | |||
677 | 40 | 40 | ||
678 | 41 | <div id="translation-teams-listing"> | 41 | <div id="translation-teams-listing"> |
679 | 42 | <h2><a name="teams"></a>Translation teams</h2> | 42 | <h2><a name="teams"></a>Translation teams</h2> |
681 | 43 | <tal:translators condition="context/translators"> | 43 | <tal:translator-list define="translator_list view/translator_list"> |
682 | 44 | <tal:translators condition="translator_list"> | ||
683 | 44 | <table class="sortable listing" id="group-members"> | 45 | <table class="sortable listing" id="group-members"> |
684 | 45 | <thead> | 46 | <thead> |
685 | 46 | <tr> | 47 | <tr> |
686 | @@ -48,11 +49,11 @@ | |||
687 | 48 | <th>Team/Supervisor</th> | 49 | <th>Team/Supervisor</th> |
688 | 49 | <th>Guidelines</th> | 50 | <th>Guidelines</th> |
689 | 50 | <th>Appointed</th> | 51 | <th>Appointed</th> |
691 | 51 | <th tal:condition="context/required:launchpad.Edit"></th> | 52 | <th tal:condition="view/user_can_edit"></th> |
692 | 52 | </tr> | 53 | </tr> |
693 | 53 | </thead> | 54 | </thead> |
694 | 54 | <tbody> | 55 | <tbody> |
696 | 55 | <tr tal:repeat="translator view/translator_list"> | 56 | <tr tal:repeat="translator translator_list"> |
697 | 56 | <td class="translator-language"> | 57 | <td class="translator-language"> |
698 | 57 | <a tal:attributes="href translator/language/fmt:url" | 58 | <a tal:attributes="href translator/language/fmt:url" |
699 | 58 | tal:content="translator/language/displayname"> | 59 | tal:content="translator/language/displayname"> |
700 | @@ -82,7 +83,7 @@ | |||
701 | 82 | none | 83 | none |
702 | 83 | </span> | 84 | </span> |
703 | 84 | <tal:notadmin | 85 | <tal:notadmin |
705 | 85 | condition="not:context/required:launchpad.Edit"> | 86 | condition="not:view/user_can_edit"> |
706 | 86 | <a tal:condition=" | 87 | <a tal:condition=" |
707 | 87 | translator/context/required:launchpad.Edit" | 88 | translator/context/required:launchpad.Edit" |
708 | 88 | tal:attributes="href string:${translator/code}/+edit" | 89 | tal:attributes="href string:${translator/code}/+edit" |
709 | @@ -96,7 +97,7 @@ | |||
710 | 96 | 2007-09-17 | 97 | 2007-09-17 |
711 | 97 | </span> | 98 | </span> |
712 | 98 | </td> | 99 | </td> |
714 | 99 | <td tal:condition="context/required:launchpad.Edit"> | 100 | <td tal:condition="view/user_can_edit"> |
715 | 100 | <a tal:attributes=" | 101 | <a tal:attributes=" |
716 | 101 | href translator/code; | 102 | href translator/code; |
717 | 102 | id string:edit-${translator/code}-translator" | 103 | id string:edit-${translator/code}-translator" |
718 | @@ -110,22 +111,22 @@ | |||
719 | 110 | </tbody> | 111 | </tbody> |
720 | 111 | </table> | 112 | </table> |
721 | 112 | </tal:translators> | 113 | </tal:translators> |
723 | 113 | <tal:no-translators condition="not: context/translators"> | 114 | <tal:no-translators condition="not: translator_list"> |
724 | 114 | No translation teams or supervisors have been appointed in | 115 | No translation teams or supervisors have been appointed in |
725 | 115 | this group yet. | 116 | this group yet. |
726 | 116 | </tal:no-translators> | 117 | </tal:no-translators> |
727 | 117 | <div style="margin-top:1em; margin-bottom: 2em;"> | 118 | <div style="margin-top:1em; margin-bottom: 2em;"> |
729 | 118 | <a tal:condition="context/required:launchpad.Edit" | 119 | <a tal:condition="view/user_can_edit" |
730 | 119 | href="+appoint" class="add sprite">Appoint a new translation | 120 | href="+appoint" class="add sprite">Appoint a new translation |
731 | 120 | team</a> | 121 | team</a> |
732 | 121 | </div> | 122 | </div> |
740 | 122 | </div><!-- id="translations-team-listing" --> | 123 | </tal:translator-list> |
741 | 123 | 124 | </div><!-- id="translations-team-listing" --> | |
742 | 124 | <div class="section"> | 125 | |
743 | 125 | <a name="projects"></a> | 126 | <div class="section"> |
744 | 126 | <div tal:replace="structure context/@@+portlet-projects" /> | 127 | <a name="projects"></a> |
745 | 127 | </div> | 128 | <div tal:replace="structure context/@@+portlet-projects" /> |
746 | 128 | 129 | </div> | |
747 | 129 | </div><!-- main --> | 130 | </div><!-- main --> |
748 | 130 | 131 | ||
749 | 131 | </body> | 132 | </body> |
750 | 132 | 133 | ||
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 | 13 | <a href="#teams">translation teams</a>. | 13 | <a href="#teams">translation teams</a>. |
756 | 14 | </p> | 14 | </p> |
757 | 15 | 15 | ||
760 | 16 | <div id="related-projects"> | 16 | <div id="related-projects" |
761 | 17 | <div tal:condition="context/distributions" style="margin-top:1em;"> | 17 | tal:define=" |
762 | 18 | distributions context/fetchDistrosForDisplay; | ||
763 | 19 | projectgroups context/fetchProjectGroupsForDisplay; | ||
764 | 20 | projects context/fetchProjectsForDisplay"> | ||
765 | 21 | <div tal:condition="distributions" style="margin-top:1em;"> | ||
766 | 18 | <h3 style="display: inline;">Distributions:</h3> | 22 | <h3 style="display: inline;">Distributions:</h3> |
769 | 19 | <tal:distribution | 23 | <tal:distribution repeat="distribution distributions"> |
768 | 20 | repeat="distribution context/distributions"> | ||
770 | 21 | <a href="#" tal:replace="structure distribution/fmt:link">Ubuntu | 24 | <a href="#" tal:replace="structure distribution/fmt:link">Ubuntu |
771 | 22 | </a><tal:comma condition="not:repeat/distribution/end">, </tal:comma> | 25 | </a><tal:comma condition="not:repeat/distribution/end">, </tal:comma> |
772 | 23 | </tal:distribution> | 26 | </tal:distribution> |
773 | 24 | </div> | 27 | </div> |
774 | 25 | 28 | ||
776 | 26 | <div tal:condition="context/projects" style="margin-top:1em;"> | 29 | <div tal:condition="projectgroups" style="margin-top:1em;"> |
777 | 27 | <h3 style="display: inline;">Project groups:</h3> | 30 | <h3 style="display: inline;">Project groups:</h3> |
780 | 28 | <tal:project | 31 | <tal:projectgroup repeat="projectgroup projectgroups"> |
779 | 29 | repeat="projectgroup context/projects"> | ||
781 | 30 | <a href="#" tal:replace="structure projectgroup/fmt:link">GNOME | 32 | <a href="#" tal:replace="structure projectgroup/fmt:link">GNOME |
782 | 31 | </a><tal:comma condition="not:repeat/projectgroup/end">, </tal:comma> | 33 | </a><tal:comma condition="not:repeat/projectgroup/end">, </tal:comma> |
783 | 34 | </tal:projectgroup> | ||
784 | 35 | </div> | ||
785 | 36 | |||
786 | 37 | <div tal:condition="projects" style="margin-top:1em;"> | ||
787 | 38 | <h3 style="display: inline;">Projects:</h3> | ||
788 | 39 | <tal:project repeat="project projects"> | ||
789 | 40 | <a href="#" tal:replace="structure project/fmt:link">Firefox | ||
790 | 41 | </a><tal:comma condition="not:repeat/project/end">, </tal:comma> | ||
791 | 32 | </tal:project> | 42 | </tal:project> |
792 | 33 | </div> | 43 | </div> |
793 | 34 | 44 | ||
794 | 35 | <div tal:condition="context/products" style="margin-top:1em;"> | ||
795 | 36 | <h3 style="display: inline;">Projects:</h3> | ||
796 | 37 | <tal:product | ||
797 | 38 | repeat="product context/products"> | ||
798 | 39 | <a href="#" tal:replace="structure product/fmt:link">Firefox | ||
799 | 40 | </a><tal:comma condition="not:repeat/product/end">, </tal:comma> | ||
800 | 41 | </tal:product> | ||
801 | 42 | </div> | ||
802 | 43 | |||
803 | 44 | </div> | 45 | </div> |
804 | 45 | </tal:root> | 46 | </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...