Merge lp:~jtv/launchpad/bug-327575 into lp:launchpad
- bug-327575
- Merge into devel
Proposed by
Jeroen T. Vermeulen
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Jeroen T. Vermeulen | ||||
Approved revision: | no longer in the source branch. | ||||
Merged at revision: | not available | ||||
Proposed branch: | lp:~jtv/launchpad/bug-327575 | ||||
Merge into: | lp:launchpad | ||||
Diff against target: |
321 lines (+173/-34) 6 files modified
lib/lp/translations/browser/tests/test_translationgroup.py (+69/-0) lib/lp/translations/browser/translationgroup.py (+23/-16) lib/lp/translations/doc/translationgroup.txt (+37/-0) lib/lp/translations/interfaces/translationgroup.py (+14/-4) lib/lp/translations/model/translationgroup.py (+27/-12) lib/lp/translations/templates/translationgroup-index.pt (+3/-2) |
||||
To merge this branch: | bzr merge lp:~jtv/launchpad/bug-327575 | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Muharem Hrnjadovic (community) | code | Approve | |
Review via email: mp+20591@code.launchpad.net |
Commit message
Speed up TranslationGrou
Description of the change
To post a comment you must log in.
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote : | # |
Revision history for this message
Muharem Hrnjadovic (al-maisan) wrote : | # |
Nice branch!
review:
Approve
(code)
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === added file 'lib/lp/translations/browser/tests/test_translationgroup.py' | |||
2 | --- lib/lp/translations/browser/tests/test_translationgroup.py 1970-01-01 00:00:00 +0000 | |||
3 | +++ lib/lp/translations/browser/tests/test_translationgroup.py 2010-03-03 20:17:26 +0000 | |||
4 | @@ -0,0 +1,69 @@ | |||
5 | 1 | # Copyright 2010 Canonical Ltd. This software is licensed under the | ||
6 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
7 | 3 | |||
8 | 4 | """View tests for TranslationGroup.""" | ||
9 | 5 | |||
10 | 6 | __metaclass__ = type | ||
11 | 7 | |||
12 | 8 | import unittest | ||
13 | 9 | |||
14 | 10 | from zope.component import getUtility | ||
15 | 11 | |||
16 | 12 | from canonical.testing import LaunchpadZopelessLayer | ||
17 | 13 | |||
18 | 14 | from canonical.launchpad.webapp.servers import LaunchpadTestRequest | ||
19 | 15 | from lp.services.worlddata.interfaces.language import ILanguageSet | ||
20 | 16 | from lp.translations.browser.translationgroup import TranslationGroupView | ||
21 | 17 | from lp.testing import TestCaseWithFactory | ||
22 | 18 | |||
23 | 19 | |||
24 | 20 | class TestTranslationGroupView(TestCaseWithFactory): | ||
25 | 21 | layer = LaunchpadZopelessLayer | ||
26 | 22 | |||
27 | 23 | def setUp(self): | ||
28 | 24 | super(TestTranslationGroupView, self).setUp() | ||
29 | 25 | |||
30 | 26 | def _makeGroup(self): | ||
31 | 27 | """Create a translation group.""" | ||
32 | 28 | return self.factory.makeTranslationGroup() | ||
33 | 29 | |||
34 | 30 | def _makeView(self, group): | ||
35 | 31 | """Create a view for self.group.""" | ||
36 | 32 | view = TranslationGroupView(group, LaunchpadTestRequest()) | ||
37 | 33 | view.initialize() | ||
38 | 34 | return view | ||
39 | 35 | |||
40 | 36 | def test_translator_list_empty(self): | ||
41 | 37 | view = self._makeView(self._makeGroup()) | ||
42 | 38 | self.assertEqual([], view.translator_list) | ||
43 | 39 | |||
44 | 40 | def test_translator_list(self): | ||
45 | 41 | # translator_list composes dicts using _makeTranslatorDict. | ||
46 | 42 | group = self._makeGroup() | ||
47 | 43 | tr_translator = self.factory.makeTranslator('tr', group) | ||
48 | 44 | view = self._makeView(group) | ||
49 | 45 | translator_dict = view._makeTranslatorDict( | ||
50 | 46 | tr_translator, tr_translator.language, tr_translator.translator) | ||
51 | 47 | self.assertEqual([translator_dict], list(view.translator_list)) | ||
52 | 48 | |||
53 | 49 | def test_makeTranslatorDict(self): | ||
54 | 50 | # _makeTranslatorDict describes a Translator entry to the UI. | ||
55 | 51 | group = self._makeGroup() | ||
56 | 52 | xhosa = self.factory.makeTranslator('xh', group) | ||
57 | 53 | xhosa.style_guide_url = 'http://xh.example.com/' | ||
58 | 54 | view = self._makeView(group) | ||
59 | 55 | output = view._makeTranslatorDict( | ||
60 | 56 | xhosa, xhosa.language, xhosa.translator) | ||
61 | 57 | |||
62 | 58 | self.assertEqual(xhosa.translator, output['person']) | ||
63 | 59 | self.assertEqual('xh', output['code']) | ||
64 | 60 | self.assertEqual( | ||
65 | 61 | getUtility(ILanguageSet).getLanguageByCode('xh'), | ||
66 | 62 | output['language']) | ||
67 | 63 | self.assertEqual(xhosa.datecreated, output['datecreated']) | ||
68 | 64 | self.assertEqual(xhosa.style_guide_url, output['style_guide_url']) | ||
69 | 65 | self.assertEqual(xhosa, output['context']) | ||
70 | 66 | |||
71 | 67 | |||
72 | 68 | def test_suite(): | ||
73 | 69 | return unittest.TestLoader().loadTestsFromName(__name__) | ||
74 | 0 | 70 | ||
75 | === modified file 'lib/lp/translations/browser/translationgroup.py' | |||
76 | --- lib/lp/translations/browser/translationgroup.py 2010-02-17 15:50:00 +0000 | |||
77 | +++ lib/lp/translations/browser/translationgroup.py 2010-03-03 20:17:26 +0000 | |||
78 | @@ -16,8 +16,6 @@ | |||
79 | 16 | 'TranslationGroupView', | 16 | 'TranslationGroupView', |
80 | 17 | ] | 17 | ] |
81 | 18 | 18 | ||
82 | 19 | import operator | ||
83 | 20 | |||
84 | 21 | from zope.component import getUtility | 19 | from zope.component import getUtility |
85 | 22 | 20 | ||
86 | 23 | from lp.translations.interfaces.translationgroup import ( | 21 | from lp.translations.interfaces.translationgroup import ( |
87 | @@ -29,7 +27,7 @@ | |||
88 | 29 | from canonical.launchpad.webapp.interfaces import NotFoundError | 27 | from canonical.launchpad.webapp.interfaces import NotFoundError |
89 | 30 | from canonical.launchpad.webapp import ( | 28 | from canonical.launchpad.webapp import ( |
90 | 31 | action, canonical_url, GetitemNavigation, LaunchpadEditFormView, | 29 | action, canonical_url, GetitemNavigation, LaunchpadEditFormView, |
92 | 32 | LaunchpadFormView) | 30 | LaunchpadFormView, LaunchpadView) |
93 | 33 | from canonical.launchpad.webapp.breadcrumb import Breadcrumb | 31 | from canonical.launchpad.webapp.breadcrumb import Breadcrumb |
94 | 34 | 32 | ||
95 | 35 | 33 | ||
96 | @@ -54,9 +52,10 @@ | |||
97 | 54 | label = page_title | 52 | label = page_title |
98 | 55 | 53 | ||
99 | 56 | 54 | ||
101 | 57 | class TranslationGroupView: | 55 | class TranslationGroupView(LaunchpadView): |
102 | 58 | 56 | ||
103 | 59 | def __init__(self, context, request): | 57 | def __init__(self, context, request): |
104 | 58 | super(TranslationGroupView, self).__init__(context, request) | ||
105 | 60 | self.context = context | 59 | self.context = context |
106 | 61 | self.request = request | 60 | self.request = request |
107 | 62 | self.translation_groups = getUtility(ITranslationGroupSet) | 61 | self.translation_groups = getUtility(ITranslationGroupSet) |
108 | @@ -69,20 +68,28 @@ | |||
109 | 69 | def page_title(self): | 68 | def page_title(self): |
110 | 70 | return self.context.title | 69 | return self.context.title |
111 | 71 | 70 | ||
112 | 71 | def _makeTranslatorDict(self, translator, language, person): | ||
113 | 72 | """Compose dict describing a `Translator` for UI use. | ||
114 | 73 | |||
115 | 74 | Parameters are the ones found in each item in the sequence | ||
116 | 75 | returned by `TranslationGroup.fetchTranslatorData`. | ||
117 | 76 | """ | ||
118 | 77 | return { | ||
119 | 78 | 'person': person, | ||
120 | 79 | 'code': language.code, | ||
121 | 80 | 'language': language, | ||
122 | 81 | 'datecreated': translator.datecreated, | ||
123 | 82 | 'style_guide_url': translator.style_guide_url, | ||
124 | 83 | 'context': translator, | ||
125 | 84 | } | ||
126 | 85 | |||
127 | 72 | @property | 86 | @property |
128 | 73 | def translator_list(self): | 87 | def translator_list(self): |
141 | 74 | result = [] | 88 | """List of dicts describing the translation teams.""" |
142 | 75 | for item in self.context.translators: | 89 | return [ |
143 | 76 | result.append({'lang': item.language.englishname, | 90 | self._makeTranslatorDict(*data) |
144 | 77 | 'person': item.translator, | 91 | for data in self.context.fetchTranslatorData() |
145 | 78 | 'code': item.language.code, | 92 | ] |
134 | 79 | 'language': item.language, | ||
135 | 80 | 'datecreated': item.datecreated, | ||
136 | 81 | 'style_guide_url': item.style_guide_url, | ||
137 | 82 | 'context': item, | ||
138 | 83 | }) | ||
139 | 84 | result.sort(key=operator.itemgetter('lang')) | ||
140 | 85 | return result | ||
146 | 86 | 93 | ||
147 | 87 | 94 | ||
148 | 88 | class TranslationGroupAddTranslatorView(LaunchpadFormView): | 95 | class TranslationGroupAddTranslatorView(LaunchpadFormView): |
149 | 89 | 96 | ||
150 | === modified file 'lib/lp/translations/doc/translationgroup.txt' | |||
151 | --- lib/lp/translations/doc/translationgroup.txt 2009-08-25 10:20:25 +0000 | |||
152 | +++ lib/lp/translations/doc/translationgroup.txt 2010-03-03 20:17:26 +0000 | |||
153 | @@ -157,3 +157,40 @@ | |||
154 | 157 | cy | 157 | cy |
155 | 158 | foo-translators | 158 | foo-translators |
156 | 159 | None | 159 | None |
157 | 160 | |||
158 | 161 | |||
159 | 162 | fetchTranslatorData | ||
160 | 163 | ------------------- | ||
161 | 164 | |||
162 | 165 | Use fetchTranslator data to get all members of a translation group, | ||
163 | 166 | with their respective assigned languages, in one go. This saves | ||
164 | 167 | repeated querying. | ||
165 | 168 | |||
166 | 169 | >>> group = factory.makeTranslationGroup() | ||
167 | 170 | >>> list(group.fetchTranslatorData()) | ||
168 | 171 | [] | ||
169 | 172 | |||
170 | 173 | >>> de_team = factory.makeTeam(name='de-team') | ||
171 | 174 | >>> nl_team = factory.makeTeam(name='nl-team') | ||
172 | 175 | >>> la_team = factory.makeTeam(name='la-team') | ||
173 | 176 | >>> de_translator = factory.makeTranslator('de', group, person=de_team) | ||
174 | 177 | >>> nl_translator = factory.makeTranslator('nl', group, person=nl_team) | ||
175 | 178 | >>> la_translator = factory.makeTranslator('la', group, person=la_team) | ||
176 | 179 | |||
177 | 180 | The method returns tuples of respectively a Translator ("translation | ||
178 | 181 | group membership entry"), its language, and the actual team or person | ||
179 | 182 | assigned to that language. | ||
180 | 183 | |||
181 | 184 | >>> for (translator, language, team) in group.fetchTranslatorData(): | ||
182 | 185 | ... print translator.language.code, language.code, team.name | ||
183 | 186 | nl nl nl-team | ||
184 | 187 | de de de-team | ||
185 | 188 | la la la-team | ||
186 | 189 | |||
187 | 190 | The members are sorted by language name in English. | ||
188 | 191 | |||
189 | 192 | >>> for (translator, language, person) in group.fetchTranslatorData(): | ||
190 | 193 | ... print language.englishname | ||
191 | 194 | Dutch | ||
192 | 195 | German | ||
193 | 196 | Latin | ||
194 | 160 | 197 | ||
195 | === modified file 'lib/lp/translations/interfaces/translationgroup.py' | |||
196 | --- lib/lp/translations/interfaces/translationgroup.py 2010-02-02 16:02:34 +0000 | |||
197 | +++ lib/lp/translations/interfaces/translationgroup.py 2010-03-03 20:17:26 +0000 | |||
198 | @@ -1,4 +1,4 @@ | |||
200 | 1 | # Copyright 2009 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2009-2010 Canonical Ltd. This software is licensed under the |
201 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
202 | 3 | 3 | ||
203 | 4 | # pylint: disable-msg=E0211,E0213 | 4 | # pylint: disable-msg=E0211,E0213 |
204 | @@ -148,9 +148,6 @@ | |||
205 | 148 | def query_translator(language): | 148 | def query_translator(language): |
206 | 149 | """Retrieve a translator, or None, based on a Language""" | 149 | """Retrieve a translator, or None, based on a Language""" |
207 | 150 | 150 | ||
208 | 151 | def __getitem__(languagecode): | ||
209 | 152 | """Retrieve the translator for the given language in this group.""" | ||
210 | 153 | |||
211 | 154 | # adding and removing translators | 151 | # adding and removing translators |
212 | 155 | def remove_translator(language): | 152 | def remove_translator(language): |
213 | 156 | """Remove the translator for this language from the group.""" | 153 | """Remove the translator for this language from the group.""" |
214 | @@ -165,6 +162,19 @@ | |||
215 | 165 | number_of_remaining_projects = Attribute( | 162 | number_of_remaining_projects = Attribute( |
216 | 166 | "Count of remaining projects not listed in the `top_projects`.") | 163 | "Count of remaining projects not listed in the `top_projects`.") |
217 | 167 | 164 | ||
218 | 165 | def __getitem__(language_code): | ||
219 | 166 | """Retrieve the translator for the given language in this group. | ||
220 | 167 | |||
221 | 168 | This is used for navigation through the group. | ||
222 | 169 | """ | ||
223 | 170 | |||
224 | 171 | def fetchTranslatorData(): | ||
225 | 172 | """Fetch translators and related data. | ||
226 | 173 | |||
227 | 174 | :return: A tuple (`Translator`, `Language`, `Person`), ordered | ||
228 | 175 | by language name in English. | ||
229 | 176 | """ | ||
230 | 177 | |||
231 | 168 | 178 | ||
232 | 169 | class ITranslationGroupSet(Interface): | 179 | class ITranslationGroupSet(Interface): |
233 | 170 | """A container for translation groups.""" | 180 | """A container for translation groups.""" |
234 | 171 | 181 | ||
235 | === modified file 'lib/lp/translations/model/translationgroup.py' | |||
236 | --- lib/lp/translations/model/translationgroup.py 2009-08-25 10:24:51 +0000 | |||
237 | +++ lib/lp/translations/model/translationgroup.py 2010-03-03 20:17:26 +0000 | |||
238 | @@ -19,14 +19,16 @@ | |||
239 | 19 | from storm.expr import Join | 19 | from storm.expr import Join |
240 | 20 | from storm.store import Store | 20 | from storm.store import Store |
241 | 21 | 21 | ||
242 | 22 | from lp.services.worlddata.interfaces.language import ILanguageSet | ||
243 | 23 | from lp.translations.interfaces.translationgroup import ( | 22 | from lp.translations.interfaces.translationgroup import ( |
244 | 24 | ITranslationGroup, ITranslationGroupSet) | 23 | ITranslationGroup, ITranslationGroupSet) |
245 | 24 | from lp.registry.model.person import Person | ||
246 | 25 | from lp.registry.model.product import Product | 25 | from lp.registry.model.product import Product |
247 | 26 | from lp.registry.model.project import Project | 26 | from lp.registry.model.project import Project |
248 | 27 | from lp.registry.model.teammembership import TeamParticipation | 27 | from lp.registry.model.teammembership import TeamParticipation |
249 | 28 | from lp.services.worlddata.model.language import Language | ||
250 | 28 | from lp.translations.model.translator import Translator | 29 | from lp.translations.model.translator import Translator |
252 | 29 | from canonical.launchpad.webapp.interfaces import NotFoundError | 30 | from canonical.launchpad.webapp.interfaces import ( |
253 | 31 | DEFAULT_FLAVOR, IStoreSelector, MAIN_STORE, NotFoundError) | ||
254 | 30 | 32 | ||
255 | 31 | from canonical.database.sqlbase import SQLBase | 33 | from canonical.database.sqlbase import SQLBase |
256 | 32 | from canonical.database.constants import DEFAULT | 34 | from canonical.database.constants import DEFAULT |
257 | @@ -61,6 +63,21 @@ | |||
258 | 61 | joinColumn='translationgroup') | 63 | joinColumn='translationgroup') |
259 | 62 | translation_guide_url = StringCol(notNull=False, default=None) | 64 | translation_guide_url = StringCol(notNull=False, default=None) |
260 | 63 | 65 | ||
261 | 66 | def __getitem__(self, language_code): | ||
262 | 67 | """See `ITranslationGroup`.""" | ||
263 | 68 | store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR) | ||
264 | 69 | query = store.find( | ||
265 | 70 | Translator, | ||
266 | 71 | Translator.translationgroup == self, | ||
267 | 72 | Translator.languageID == Language.id, | ||
268 | 73 | Language.code == language_code) | ||
269 | 74 | |||
270 | 75 | translator = query.one() | ||
271 | 76 | if translator is None: | ||
272 | 77 | raise NotFoundError, language_code | ||
273 | 78 | |||
274 | 79 | return translator | ||
275 | 80 | |||
276 | 64 | # used to note additions | 81 | # used to note additions |
277 | 65 | def add(self, content): | 82 | def add(self, content): |
278 | 66 | """See ITranslationGroup.""" | 83 | """See ITranslationGroup.""" |
279 | @@ -117,17 +134,15 @@ | |||
280 | 117 | else: | 134 | else: |
281 | 118 | return 0 | 135 | return 0 |
282 | 119 | 136 | ||
286 | 120 | 137 | def fetchTranslatorData(self): | |
284 | 121 | # get a translator by code | ||
285 | 122 | def __getitem__(self, code): | ||
287 | 123 | """See ITranslationGroup.""" | 138 | """See ITranslationGroup.""" |
295 | 124 | language_set = getUtility(ILanguageSet) | 139 | store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR) |
296 | 125 | language = language_set[code] | 140 | translator_data = store.find( |
297 | 126 | result = Translator.selectOneBy(language=language, | 141 | (Translator, Language, Person), |
298 | 127 | translationgroup=self) | 142 | Translator.translationgroup == self, |
299 | 128 | if result is None: | 143 | Language.id == Translator.languageID, |
300 | 129 | raise NotFoundError, code | 144 | Person.id == Translator.translatorID) |
301 | 130 | return result | 145 | return translator_data.order_by(Language.englishname) |
302 | 131 | 146 | ||
303 | 132 | 147 | ||
304 | 133 | class TranslationGroupSet: | 148 | class TranslationGroupSet: |
305 | 134 | 149 | ||
306 | === modified file 'lib/lp/translations/templates/translationgroup-index.pt' | |||
307 | --- lib/lp/translations/templates/translationgroup-index.pt 2009-09-17 11:28:13 +0000 | |||
308 | +++ lib/lp/translations/templates/translationgroup-index.pt 2010-03-03 20:17:26 +0000 | |||
309 | @@ -71,9 +71,10 @@ | |||
310 | 71 | <td class="translator-link"> | 71 | <td class="translator-link"> |
311 | 72 | <a tal:condition="translator/style_guide_url" | 72 | <a tal:condition="translator/style_guide_url" |
312 | 73 | tal:attributes="href translator/style_guide_url" | 73 | tal:attributes="href translator/style_guide_url" |
314 | 74 | ><img src="/@@/link" alt="Doc" | 74 | ><span alt="Doc" |
315 | 75 | tal:attributes="title translator/style_guide_url" | 75 | tal:attributes="title translator/style_guide_url" |
317 | 76 | /> | 76 | class="external-link sprite" |
318 | 77 | ></span> | ||
319 | 77 | <tal:link | 78 | <tal:link |
320 | 78 | replace="translator/style_guide_url/fmt:shorten/30" | 79 | replace="translator/style_guide_url/fmt:shorten/30" |
321 | 79 | ></tal:link></a> | 80 | ></tal:link></a> |
= Bug 327575 =
We're seeing some timeouts (and a robust ranking in the top-10 query counts) for the TranslationGrou p:+index page.
The culprit seems to be lots of Person queries, which can only be the translation teams in the group. There are also repeated queries for Language.
Here I introduce a view method that batch-fetches this information. It's an ideal candidate for batch-fetching: a simple three-way 1-to-1 join, no repetition whatsoever, no UI batching, and no selectivity beyond the "front gate" of the query.
In other news, the TranslationGrou pView becomes an adopted child of LaunchpadView, and an <img> in the template becomes a sprite.
{{{ pView -t /translationgro up.txt
./bin/test -vv -t TranslationGrou
}}}
Jeroen