Merge lp:~jtv/launchpad/bug-416434-aggregator into lp:launchpad
- bug-416434-aggregator
- Merge into devel
Proposed by
Jeroen T. Vermeulen
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Graham Binns | ||||
Approved revision: | no longer in the source branch. | ||||
Merged at revision: | not available | ||||
Proposed branch: | lp:~jtv/launchpad/bug-416434-aggregator | ||||
Merge into: | lp:launchpad | ||||
Diff against target: | None lines | ||||
To merge this branch: | bzr merge lp:~jtv/launchpad/bug-416434-aggregator | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Graham Binns (community) | gmb | Approve | |
Review via email:
|
Commit message
Description of the change
To post a comment you must log in.
Revision history for this message
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Jeroen T. Vermeulen (jtv) wrote : | # |
Revision history for this message
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Graham Binns (gmb) : | # |
review:
Approve
(gmb)
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'lib/lp/translations/browser/person.py' |
2 | --- lib/lp/translations/browser/person.py 2009-08-19 14:25:32 +0000 |
3 | +++ lib/lp/translations/browser/person.py 2009-08-25 10:44:02 +0000 |
4 | @@ -26,6 +26,9 @@ |
5 | from canonical.launchpad.webapp.publisher import LaunchpadView |
6 | from canonical.widgets import LaunchpadRadioWidget |
7 | from lp.registry.interfaces.person import IPerson |
8 | +from lp.registry.interfaces.sourcepackage import ISourcePackage |
9 | +from lp.translations.browser.translationlinksaggregator import ( |
10 | + TranslationLinksAggregator) |
11 | from lp.translations.interfaces.pofiletranslator import ( |
12 | IPOFileTranslatorSet) |
13 | from lp.translations.interfaces.translationrelicensingagreement import ( |
14 | @@ -33,7 +36,47 @@ |
15 | TranslationRelicensingAgreementOptions) |
16 | from lp.translations.interfaces.translationsperson import ( |
17 | ITranslationsPerson) |
18 | -from lp.translations.model.productserieslanguage import ProductSeriesLanguage |
19 | + |
20 | + |
21 | +class WorkListLinksAggregator(TranslationLinksAggregator): |
22 | + """Aggregate translation links for translation or review. |
23 | + |
24 | + Here, all files are actually `POFile`s, never `POTemplate`s. |
25 | + """ |
26 | + |
27 | + def countStrings(self, pofile): |
28 | + """Count number of strings that need work.""" |
29 | + raise NotImplementedError() |
30 | + |
31 | + def describe(self, target, link, covered_files): |
32 | + """See `TranslationLinksAggregator.describe`.""" |
33 | + strings_count = sum( |
34 | + [self.countStrings(pofile) for pofile in covered_files]) |
35 | + |
36 | + if strings_count == 1: |
37 | + strings_wording = "%d string" |
38 | + else: |
39 | + strings_wording = "%d strings" |
40 | + |
41 | + return { |
42 | + 'target': target, |
43 | + 'count': strings_count, |
44 | + 'count_wording': strings_wording % strings_count, |
45 | + 'is_product': not ISourcePackage.providedBy(target), |
46 | + 'link': link, |
47 | + } |
48 | + |
49 | + |
50 | +class ReviewLinksAggregator(WorkListLinksAggregator): |
51 | + """A `TranslationLinksAggregator` for translations to review.""" |
52 | + |
53 | + # Link to unreviewed suggestions. |
54 | + pofile_link_suffix = '/+translate?show=new_suggestions' |
55 | + |
56 | + # Strings that need work are ones with unreviewed suggestions. |
57 | + def countStrings(self, pofile): |
58 | + """See `WorkListLinksAggregator.countStrings`.""" |
59 | + return pofile.unreviewedCount() |
60 | |
61 | |
62 | class PersonTranslationsMenu(NavigationMenu): |
63 | @@ -98,6 +141,20 @@ |
64 | """Is this person in a translation group?""" |
65 | return len(self.translation_groups) != 0 |
66 | |
67 | + @property |
68 | + def person_is_translator(self): |
69 | + """Is this person active in translations?""" |
70 | + return self.context.hasKarma('translations') |
71 | + |
72 | + @property |
73 | + def person_includes_me(self): |
74 | + """Is the current user (a member of) this person?""" |
75 | + user = getUtility(ILaunchBag).user |
76 | + if user is None: |
77 | + return False |
78 | + else: |
79 | + return user.inTeam(self.context) |
80 | + |
81 | def should_display_message(self, translationmessage): |
82 | """Should a certain `TranslationMessage` be displayed. |
83 | |
84 | @@ -111,167 +168,51 @@ |
85 | return not ( |
86 | translationmessage.potmsgset.hide_translations_from_anonymous) |
87 | |
88 | - def _composeReviewLinks(self, pofiles): |
89 | - """Compose URLs for reviewing given `POFile`s.""" |
90 | - return [ |
91 | - canonical_url(pofile) + '/+translate?show=new_suggestions' |
92 | - for pofile in pofiles |
93 | - ] |
94 | - |
95 | - def _findBestCommonReviewLinks(self, pofiles): |
96 | - """Find best links to a bunch of related `POFile`s. |
97 | - |
98 | - The `POFile`s must either be in the same `Product`, or in the |
99 | - same `DistroSeries` and `SourcePackageName`. |
100 | - |
101 | - This method finds the greatest common denominators between them, |
102 | - and returns a list of links to them: the individual translation |
103 | - if there is only one, the template if multiple translations of |
104 | - one template are involved, and so on. |
105 | - """ |
106 | - assert pofiles, "Empty POFiles list in reviewable target." |
107 | - first_pofile = pofiles[0] |
108 | - |
109 | - if len(pofiles) == 1: |
110 | - # Simple case: one translation file. Go straight to |
111 | - # translation page for its unreviewed strings. |
112 | - return self._composeReviewLinks(pofiles) |
113 | - |
114 | - templates = set(pofile.potemplate for pofile in pofiles) |
115 | - |
116 | - productseries = set( |
117 | - template.productseries |
118 | - for template in templates |
119 | - if template.productseries) |
120 | - |
121 | - products = set(series.product for series in productseries) |
122 | - |
123 | - sourcepackagenames = set( |
124 | - template.sourcepackagename |
125 | - for template in templates |
126 | - if template.sourcepackagename) |
127 | - |
128 | - distroseries = set( |
129 | - template.distroseries |
130 | - for template in templates |
131 | - if template.distroseries) |
132 | - |
133 | - assert len(products) <= 1, "Got more than one product." |
134 | - assert len(sourcepackagenames) <= 1, "Got more than one package." |
135 | - assert len(distroseries) <= 1, "Got more than one distroseries." |
136 | - assert len(products) + len(sourcepackagenames) == 1, ( |
137 | - "Didn't get POFiles for exactly one package or one product.") |
138 | - |
139 | - first_template = first_pofile.potemplate |
140 | - |
141 | - if len(templates) == 1: |
142 | - # Multiple translations for one template. Link to the |
143 | - # template. |
144 | - return [canonical_url(first_template)] |
145 | - |
146 | - if sourcepackagenames: |
147 | - # Multiple POFiles for a source package. Show its template |
148 | - # listing. |
149 | - distroseries = first_template.distroseries |
150 | - packagename = first_template.sourcepackagename |
151 | - return [canonical_url(distroseries.getSourcePackage(packagename))] |
152 | - |
153 | - if len(productseries) == 1: |
154 | - series = first_template.productseries |
155 | - # All for the same ProductSeries. |
156 | - languages = set(pofile.language for pofile in pofiles) |
157 | - if len(languages) == 1: |
158 | - # All for the same language in the same ProductSeries, |
159 | - # but apparently for different templates. Link to |
160 | - # ProductSeriesLanguage. |
161 | - productserieslanguage = ProductSeriesLanguage( |
162 | - series, pofiles[0].language) |
163 | - return [canonical_url(productserieslanguage)] |
164 | - else: |
165 | - # Multiple templates and languages in the same product |
166 | - # series. Show its templates listing. |
167 | - return [canonical_url(series)] |
168 | - |
169 | - # Different release series of the same product. Link to each of |
170 | - # the individual POFiles. |
171 | - return self._composeReviewLinks(pofiles) |
172 | - |
173 | - def _describeReviewableTarget(self, target, link, strings_count): |
174 | - """Produce dict to describe a reviewable target. |
175 | - |
176 | - The target may be a `Product` or a tuple of `SourcePackageName` |
177 | - and `DistroSeries`. |
178 | - """ |
179 | - if isinstance(target, tuple): |
180 | - (name, distroseries) = target |
181 | - target = distroseries.getSourcePackage(name) |
182 | - is_product = False |
183 | - else: |
184 | - is_product = True |
185 | - |
186 | - if strings_count == 1: |
187 | - strings_wording = '%d string' |
188 | - else: |
189 | - strings_wording = '%d strings' |
190 | - |
191 | - return { |
192 | - 'target': target, |
193 | - 'count': strings_count, |
194 | - 'link': link, |
195 | - 'count_wording': strings_wording % strings_count, |
196 | - 'is_product': is_product, |
197 | - } |
198 | - |
199 | def _setHistoryHorizon(self): |
200 | """If not already set, set `self.history_horizon`.""" |
201 | if self.history_horizon is None: |
202 | now = datetime.now(pytz.timezone('UTC')) |
203 | self.history_horizon = now - timedelta(90, 0, 0) |
204 | |
205 | - def _aggregateTranslationTargets(self, pofiles): |
206 | - """Aggregate list of `POFile`s into sensible targets. |
207 | - |
208 | - Returns a list of target descriptions as returned by |
209 | - `_describeReviewableTarget` after going through |
210 | - `_findBestCommonReviewLinks`. |
211 | - """ |
212 | - targets = {} |
213 | - for pofile in pofiles: |
214 | - template = pofile.potemplate |
215 | - if template.productseries: |
216 | - target = template.productseries.product |
217 | - else: |
218 | - target = (template.sourcepackagename, template.distroseries) |
219 | - |
220 | - if target in targets: |
221 | - (count, target_pofiles) = targets[target] |
222 | - else: |
223 | - count = 0 |
224 | - target_pofiles = [] |
225 | - |
226 | - count += pofile.unreviewedCount() |
227 | - target_pofiles.append(pofile) |
228 | - |
229 | - targets[target] = (count, target_pofiles) |
230 | - |
231 | - result = [] |
232 | - for target, stats in targets.iteritems(): |
233 | - (count, target_pofiles) = stats |
234 | - links = self._findBestCommonReviewLinks(target_pofiles) |
235 | - for link in links: |
236 | - result.append( |
237 | - self._describeReviewableTarget(target, link, count)) |
238 | - |
239 | - return result |
240 | + def _getTargetsForReview(self, max_fetch=None): |
241 | + """Query and aggregate the top targets for review. |
242 | + |
243 | + :param max_fetch: Maximum number of `POFile`s to fetch while |
244 | + looking for these. |
245 | + :return: a list of at most `max_fetch` translation targets. |
246 | + Multiple `POFile`s may be aggregated together into a single |
247 | + target. |
248 | + """ |
249 | + self._setHistoryHorizon() |
250 | + person = ITranslationsPerson(self.context) |
251 | + pofiles = person.getReviewableTranslationFiles( |
252 | + no_older_than=self.history_horizon) |
253 | + |
254 | + if max_fetch is not None: |
255 | + pofiles = pofiles[:max_fetch] |
256 | + |
257 | + return ReviewLinksAggregator().aggregate(pofiles) |
258 | + |
259 | + def _suggestTargetsForReview(self, max_fetch): |
260 | + """Find random translation targets for review. |
261 | + |
262 | + :param max_fetch: Maximum number of `POFile`s to fetch while |
263 | + looking for these. |
264 | + :return: a list of at most `max_fetch` translation targets. |
265 | + Multiple `POFile`s may be aggregated together into a single |
266 | + target. |
267 | + """ |
268 | + self._setHistoryHorizon() |
269 | + person = ITranslationsPerson(self.context) |
270 | + pofiles = person.suggestReviewableTranslationFiles( |
271 | + no_older_than=self.history_horizon)[:max_fetch] |
272 | + |
273 | + return ReviewLinksAggregator().aggregate(pofiles) |
274 | |
275 | @cachedproperty |
276 | def all_projects_and_packages_to_review(self): |
277 | """Top projects and packages for this person to review.""" |
278 | - self._setHistoryHorizon() |
279 | - person = ITranslationsPerson(self.context) |
280 | - return self._aggregateTranslationTargets( |
281 | - person.getReviewableTranslationFiles( |
282 | - no_older_than=self.history_horizon)) |
283 | + return self._getTargetsForReview() |
284 | |
285 | @property |
286 | def top_projects_and_packages_to_review(self): |
287 | @@ -280,42 +221,30 @@ |
288 | |
289 | # Maximum number of projects/packages to list that this person |
290 | # has recently worked on. |
291 | - max_old_targets = 9 |
292 | + max_known_targets = 9 |
293 | # Length of overall list to display. |
294 | list_length = 10 |
295 | |
296 | # Start out with the translations that the person has recently |
297 | # worked on. Aggregation may reduce the number we get, so ask |
298 | # the database for a few extra. |
299 | - fetch = 5 * max_old_targets |
300 | - recent = self.all_projects_and_packages_to_review[:fetch] |
301 | + fetch = 5 * max_known_targets |
302 | + recent = self._getTargetsForReview(fetch)[:max_known_targets] |
303 | |
304 | # Fill out the list with other translations that the person |
305 | # could also be reviewing. |
306 | - empty_slots = list_length - min(len(recent), max_old_targets) |
307 | + empty_slots = list_length - len(recent) |
308 | fetch = 5 * empty_slots |
309 | - |
310 | - person = ITranslationsPerson(self.context) |
311 | - random_suggestions = self._aggregateTranslationTargets( |
312 | - person.suggestReviewableTranslationFiles( |
313 | - no_older_than=self.history_horizon)[:fetch]) |
314 | - |
315 | - return recent[:max_old_targets] + random_suggestions[:empty_slots] |
316 | + random_suggestions = self._suggestTargetsForReview(fetch) |
317 | + random_suggestions = random_suggestions[:empty_slots] |
318 | + |
319 | + return recent + random_suggestions |
320 | |
321 | @cachedproperty |
322 | def num_projects_and_packages_to_review(self): |
323 | """How many translations do we suggest for reviewing?""" |
324 | return len(self.all_projects_and_packages_to_review) |
325 | |
326 | - @property |
327 | - def person_includes_me(self): |
328 | - """Is the current user (a member of) this person?""" |
329 | - user = getUtility(ILaunchBag).user |
330 | - if user is None: |
331 | - return False |
332 | - else: |
333 | - return user.inTeam(self.context) |
334 | - |
335 | |
336 | class PersonTranslationRelicensingView(LaunchpadFormView): |
337 | """View for Person's translation relicensing page.""" |
338 | |
339 | === modified file 'lib/lp/translations/browser/tests/test_persontranslationview.py' |
340 | --- lib/lp/translations/browser/tests/test_persontranslationview.py 2009-08-19 15:20:40 +0000 |
341 | +++ lib/lp/translations/browser/tests/test_persontranslationview.py 2009-08-25 10:26:28 +0000 |
342 | @@ -14,7 +14,6 @@ |
343 | from canonical.launchpad.webapp.servers import LaunchpadTestRequest |
344 | from lp.services.worlddata.model.language import LanguageSet |
345 | from lp.translations.browser.person import PersonTranslationView |
346 | -from lp.translations.model.productserieslanguage import ProductSeriesLanguage |
347 | from lp.translations.model.translator import TranslatorSet |
348 | |
349 | |
350 | @@ -78,251 +77,6 @@ |
351 | self._makeReviewer() |
352 | self.assertTrue(self.view.person_is_reviewer) |
353 | |
354 | - def test_findBestCommonReviewLinks_single_pofile(self): |
355 | - # If passed a single POFile, _findBestCommonReviewLinks returns |
356 | - # a list of just that POFile. |
357 | - pofile = self.factory.makePOFile(language_code='lua') |
358 | - links = self.view._findBestCommonReviewLinks([pofile]) |
359 | - self.assertEqual(self.view._composeReviewLinks([pofile]), links) |
360 | - |
361 | - def test_findBestCommonReviewLinks_product_wild_mix(self): |
362 | - # A combination of wildly different POFiles in the same product |
363 | - # yields links to the individual POFiles. |
364 | - pofile1 = self.factory.makePOFile(language_code='sux') |
365 | - product = pofile1.potemplate.productseries.product |
366 | - series2 = self.factory.makeProductSeries(product) |
367 | - template2 = self.factory.makePOTemplate(productseries=series2) |
368 | - pofile2 = self.factory.makePOFile( |
369 | - potemplate=template2, language_code='la') |
370 | - |
371 | - links = self.view._findBestCommonReviewLinks([pofile1, pofile2]) |
372 | - |
373 | - expected_links = self.view._composeReviewLinks([pofile1, pofile2]) |
374 | - self.assertEqual(expected_links, links) |
375 | - |
376 | - def test_findBestCommonReviewLinks_different_templates(self): |
377 | - # A combination of POFiles in the same language but different |
378 | - # templates of the same productseries is represented as a link |
379 | - # to the ProductSeriesLanguage. |
380 | - pofile1 = self.factory.makePOFile(language_code='nl') |
381 | - series = pofile1.potemplate.productseries |
382 | - template2 = self.factory.makePOTemplate(productseries=series) |
383 | - pofile2 = self.factory.makePOFile( |
384 | - potemplate=template2, language_code='nl') |
385 | - |
386 | - links = self.view._findBestCommonReviewLinks([pofile1, pofile2]) |
387 | - |
388 | - productserieslanguage = ProductSeriesLanguage( |
389 | - series, pofile1.language) |
390 | - self.assertEqual([canonical_url(productserieslanguage)], links) |
391 | - |
392 | - def test_findBestCommonReviewLinks_different_languages(self): |
393 | - # If the POFiles differ only in language, we get a link to the |
394 | - # overview for the template. |
395 | - pofile1 = self.factory.makePOFile(language_code='nl') |
396 | - template = pofile1.potemplate |
397 | - pofile2 = self.factory.makePOFile( |
398 | - potemplate=template, language_code='lo') |
399 | - |
400 | - links = self.view._findBestCommonReviewLinks([pofile1, pofile2]) |
401 | - |
402 | - self.assertEqual([canonical_url(template)], links) |
403 | - |
404 | - def test_findBestCommonReviewLinks_sharing_pofiles(self): |
405 | - # In a Product, two POFiles may share their translations. For |
406 | - # now, we link to each individually. We may want to make this |
407 | - # more clever in the future. |
408 | - pofile1 = self.factory.makePOFile(language_code='nl') |
409 | - template1 = pofile1.potemplate |
410 | - series1 = template1.productseries |
411 | - series2 = self.factory.makeProductSeries(product=series1.product) |
412 | - template2 = self.factory.makePOTemplate( |
413 | - productseries=series2, name=template1.name, |
414 | - translation_domain=template1.translation_domain) |
415 | - pofile2 = template2.getPOFileByLang('nl') |
416 | - |
417 | - pofiles = [pofile1, pofile2] |
418 | - links = self.view._findBestCommonReviewLinks(pofiles) |
419 | - |
420 | - self.assertEqual(self.view._composeReviewLinks(pofiles), links) |
421 | - |
422 | - def test_findBestCommonReviewLinks_package_different_languages(self): |
423 | - # For package POFiles in the same template but different |
424 | - # languages, we link to the template. |
425 | - package = self.factory.makeSourcePackage() |
426 | - package.distroseries.distribution.official_rosetta = True |
427 | - template = self.factory.makePOTemplate( |
428 | - distroseries=package.distroseries, |
429 | - sourcepackagename=package.sourcepackagename) |
430 | - pofile1 = self.factory.makePOFile( |
431 | - potemplate=template, language_code='nl') |
432 | - pofile2 = self.factory.makePOFile( |
433 | - potemplate=template, language_code='ka') |
434 | - |
435 | - links = self.view._findBestCommonReviewLinks([pofile1, pofile2]) |
436 | - self.assertEqual([canonical_url(template)], links) |
437 | - |
438 | - def test_findBestCommonReviewLinks_package_different_templates(self): |
439 | - # For package POFiles in different templates, we to the |
440 | - # package's template list. There is no "source package series |
441 | - # language" page. |
442 | - package = self.factory.makeSourcePackage() |
443 | - package.distroseries.distribution.official_rosetta = True |
444 | - template1 = self.factory.makePOTemplate( |
445 | - distroseries=package.distroseries, |
446 | - sourcepackagename=package.sourcepackagename) |
447 | - template2 = self.factory.makePOTemplate( |
448 | - distroseries=package.distroseries, |
449 | - sourcepackagename=package.sourcepackagename) |
450 | - pofile1 = self.factory.makePOFile( |
451 | - potemplate=template1, language_code='nl') |
452 | - pofile2 = self.factory.makePOFile( |
453 | - potemplate=template2, language_code='nl') |
454 | - |
455 | - links = self.view._findBestCommonReviewLinks([pofile1, pofile2]) |
456 | - |
457 | - self.assertEqual([canonical_url(package)], links) |
458 | - |
459 | - def test_describeReviewableTarget_string_count(self): |
460 | - # _describeReviewableTarget puts out a human-readable |
461 | - # description of how many strings need review. |
462 | - product = self.factory.makeProduct() |
463 | - |
464 | - description = self.view._describeReviewableTarget( |
465 | - product, canonical_url(product), 0) |
466 | - self.assertEqual(description['count'], 0) |
467 | - self.assertEqual(description['count_wording'], '0 strings') |
468 | - |
469 | - # Singular applies for exactly 1 string. |
470 | - description = self.view._describeReviewableTarget( |
471 | - product, canonical_url(product), 1) |
472 | - self.assertEqual(description['count'], 1) |
473 | - self.assertEqual(description['count_wording'], '1 string') |
474 | - |
475 | - |
476 | - description = self.view._describeReviewableTarget( |
477 | - product, canonical_url(product), 2) |
478 | - self.assertEqual(description['count'], 2) |
479 | - self.assertEqual(description['count_wording'], '2 strings') |
480 | - |
481 | - def test_describeReviewableTarget_product(self): |
482 | - # _describeReviewableTarget describes a Product with reviewable |
483 | - # translations. |
484 | - product = self.factory.makeProduct() |
485 | - link = canonical_url(product) |
486 | - |
487 | - description = self.view._describeReviewableTarget(product, link, 99) |
488 | - |
489 | - expected_description = { |
490 | - 'target': product, |
491 | - 'count': 99, |
492 | - 'count_wording': "99 strings", |
493 | - 'is_product': True, |
494 | - 'link': link, |
495 | - } |
496 | - self.assertEqual(expected_description, description) |
497 | - |
498 | - def test_describeReviewableTarget_package(self): |
499 | - # _describeReviewableTarget describes a package with reviewable |
500 | - # translations. |
501 | - package = self.factory.makeSourcePackage() |
502 | - package.distroseries.distribution.official_rosetta = True |
503 | - target = (package.sourcepackagename, package.distroseries) |
504 | - link = canonical_url(package) |
505 | - |
506 | - description = self.view._describeReviewableTarget(target, link, 42) |
507 | - |
508 | - expected_description = { |
509 | - 'target': package, |
510 | - 'count': 42, |
511 | - 'count_wording': "42 strings", |
512 | - 'is_product': False, |
513 | - 'link': link, |
514 | - } |
515 | - self.assertEqual(expected_description, description) |
516 | - |
517 | - def test_aggregateTranslationTargets(self): |
518 | - # _aggregateTranslationTargets represents a series of POFiles as |
519 | - # a series of target descriptions, aggregating where possible. |
520 | - |
521 | - # Trivial case: no POFiles means no targets. |
522 | - self.assertEqual([], self.view._aggregateTranslationTargets([])) |
523 | - |
524 | - # Basic case: one POFile yields its product or package. |
525 | - pofile = self.factory.makePOFile(language_code='ca') |
526 | - |
527 | - description = self.view._aggregateTranslationTargets([pofile]) |
528 | - |
529 | - expected_links = self.view._composeReviewLinks([pofile]) |
530 | - expected_description = [{ |
531 | - 'target': pofile.potemplate.productseries.product, |
532 | - 'count': 0, |
533 | - 'count_wording': "0 strings", |
534 | - 'is_product': True, |
535 | - 'link': expected_links[0], |
536 | - }] |
537 | - self.assertEqual(expected_description, description) |
538 | - |
539 | - def test_aggregateTranslationTargets_product_and_package(self): |
540 | - # _aggregateTranslationTargets keeps a product and a package |
541 | - # separate. |
542 | - product_pofile = self.factory.makePOFile(language_code='th') |
543 | - removeSecurityProxy(product_pofile).unreviewed_count = 1 |
544 | - |
545 | - package = self.factory.makeSourcePackage() |
546 | - package.distroseries.distribution.official_rosetta = True |
547 | - package_template = self.factory.makePOTemplate( |
548 | - distroseries=package.distroseries, |
549 | - sourcepackagename=package.sourcepackagename) |
550 | - package_pofile = self.factory.makePOFile( |
551 | - potemplate=package_template, language_code='th') |
552 | - removeSecurityProxy(package_pofile).unreviewed_count = 2 |
553 | - |
554 | - descriptions = self.view._aggregateTranslationTargets( |
555 | - [product_pofile, package_pofile]) |
556 | - links = set(entry['link'] for entry in descriptions) |
557 | - |
558 | - expected_links = set( |
559 | - self.view._composeReviewLinks([product_pofile, package_pofile])) |
560 | - self.assertEqual(expected_links, links) |
561 | - |
562 | - def test_aggregateTranslationTargets_bundles_productseries(self): |
563 | - # _aggregateTranslationTargets describes POFiles for the same |
564 | - # ProductSeries together. |
565 | - pofile1 = self.factory.makePOFile(language_code='es') |
566 | - series = pofile1.potemplate.productseries |
567 | - template2 = self.factory.makePOTemplate(productseries=series) |
568 | - pofile2 = self.factory.makePOFile( |
569 | - language_code='br', potemplate=template2) |
570 | - |
571 | - description = self.view._aggregateTranslationTargets( |
572 | - [pofile1, pofile2]) |
573 | - |
574 | - self.assertEqual(1, len(description)) |
575 | - self.assertEqual(canonical_url(series), description[0]['link']) |
576 | - |
577 | - def test_aggregateTranslationTargets_bundles_package(self): |
578 | - # _aggregateTranslationTargets describes POFiles for the same |
579 | - # ProductSeries together. |
580 | - package = self.factory.makeSourcePackage() |
581 | - package.distroseries.distribution.official_rosetta = True |
582 | - template1 = self.factory.makePOTemplate( |
583 | - distroseries=package.distroseries, |
584 | - sourcepackagename=package.sourcepackagename) |
585 | - pofile1 = self.factory.makePOFile( |
586 | - language_code='es', potemplate=template1) |
587 | - template2 = self.factory.makePOTemplate( |
588 | - distroseries=package.distroseries, |
589 | - sourcepackagename=package.sourcepackagename) |
590 | - pofile2 = self.factory.makePOFile( |
591 | - language_code='br', potemplate=template2) |
592 | - |
593 | - description = self.view._aggregateTranslationTargets( |
594 | - [pofile1, pofile2]) |
595 | - |
596 | - self.assertEqual(1, len(description)) |
597 | - self.assertEqual(canonical_url(package), description[0]['link']) |
598 | - |
599 | def test_num_projects_and_packages_to_review(self): |
600 | # num_projects_and_packages_to_review counts the number of |
601 | # reviewable targets that the person has worked on. |
602 | @@ -332,6 +86,50 @@ |
603 | |
604 | self.assertEqual(1, self.view.num_projects_and_packages_to_review) |
605 | |
606 | + def test_all_projects_and_packages_to_review_one(self): |
607 | + # all_projects_and_packages describes the translations available |
608 | + # for review by its person. |
609 | + self._makeReviewer() |
610 | + pofile = self._makePOFiles(1, previously_worked_on=True)[0] |
611 | + product = pofile.potemplate.productseries.product |
612 | + |
613 | + descriptions = self.view.all_projects_and_packages_to_review |
614 | + |
615 | + self.assertEqual(1, len(descriptions)) |
616 | + self.assertEqual(product, descriptions[0]['target']) |
617 | + |
618 | + def test_all_projects_and_packages_to_review_none(self): |
619 | + # all_projects_and_packages_to_review works even if there is |
620 | + # nothing to review. It will find nothing. |
621 | + self._makeReviewer() |
622 | + |
623 | + descriptions = self.view.all_projects_and_packages_to_review |
624 | + |
625 | + self.assertEqual([], descriptions) |
626 | + |
627 | + def test_all_projects_and_packages_to_review_string_singular(self): |
628 | + # A translation description says how many strings need review, |
629 | + # both as a number and as text. |
630 | + self._makeReviewer() |
631 | + pofile = self._makePOFiles(1, previously_worked_on=True)[0] |
632 | + removeSecurityProxy(pofile).unreviewed_count = 1 |
633 | + |
634 | + description = self.view.all_projects_and_packages_to_review[0] |
635 | + |
636 | + self.assertEqual(1, description['count']) |
637 | + self.assertEqual("1 string", description['count_wording']) |
638 | + |
639 | + def test_all_projects_and_packages_to_review_string_plural(self): |
640 | + # For multiple strings, count_wording uses the plural. |
641 | + self._makeReviewer() |
642 | + pofile = self._makePOFiles(1, previously_worked_on=True)[0] |
643 | + removeSecurityProxy(pofile).unreviewed_count = 2 |
644 | + |
645 | + description = self.view.all_projects_and_packages_to_review[0] |
646 | + |
647 | + self.assertEqual(2, description['count']) |
648 | + self.assertEqual("2 strings", description['count_wording']) |
649 | + |
650 | def test_num_projects_and_packages_to_review_zero(self): |
651 | # num_projects_and_packages does not count new suggestions. |
652 | self._makeReviewer() |
653 | @@ -351,8 +149,11 @@ |
654 | |
655 | targets = self.view.top_projects_and_packages_to_review |
656 | |
657 | - expected_links = self.view._composeReviewLinks( |
658 | - [pofile_worked_on, pofile_not_worked_on]) |
659 | + pofile_suffix = '/+translate?show=new_suggestions' |
660 | + expected_links = [ |
661 | + canonical_url(pofile_worked_on) + pofile_suffix, |
662 | + canonical_url(pofile_not_worked_on) + pofile_suffix, |
663 | + ] |
664 | self.assertEqual( |
665 | set(expected_links), set(item['link'] for item in targets)) |
666 | |
667 | |
668 | === added file 'lib/lp/translations/browser/tests/test_translationlinksaggregator.py' |
669 | --- lib/lp/translations/browser/tests/test_translationlinksaggregator.py 1970-01-01 00:00:00 +0000 |
670 | +++ lib/lp/translations/browser/tests/test_translationlinksaggregator.py 2009-08-25 10:35:29 +0000 |
671 | @@ -0,0 +1,269 @@ |
672 | +# Copyright 2009 Canonical Ltd. This software is licensed under the |
673 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
674 | + |
675 | +__metaclass__ = type |
676 | + |
677 | +from unittest import TestLoader |
678 | + |
679 | +from zope.security.proxy import removeSecurityProxy |
680 | + |
681 | +from lp.testing import TestCaseWithFactory |
682 | +from canonical.testing import LaunchpadZopelessLayer |
683 | + |
684 | +from canonical.launchpad.webapp import canonical_url |
685 | +from lp.translations.browser.translationlinksaggregator import ( |
686 | + TranslationLinksAggregator) |
687 | +from lp.translations.model.productserieslanguage import ProductSeriesLanguage |
688 | + |
689 | + |
690 | +class DumbAggregator(TranslationLinksAggregator): |
691 | + """A very simple `TranslationLinksAggregator`. |
692 | + |
693 | + The `describe` method returns a tuple of its arguments. |
694 | + """ |
695 | + def describe(self, target, link, covered_sheets): |
696 | + """See `TranslationLinksAggregator`.""" |
697 | + return (target, link, covered_sheets) |
698 | + |
699 | + |
700 | +def map_link(link_target, sheets=None, add_to=None): |
701 | + """Map a link the way _circumscribe does. |
702 | + |
703 | + :param link_target: The object to link to. Its URL will be used. |
704 | + :param sheets: A list of POFiles and/or POTemplates. The link will |
705 | + map to these. If omitted, the list will consist of |
706 | + `link_target` itself. |
707 | + :param add_to: Optional existing dict to add the new entry to. |
708 | + :return: A dict mapping the URL for link_target to sheets. |
709 | + """ |
710 | + if add_to is None: |
711 | + add_to = {} |
712 | + if sheets is None: |
713 | + sheets = [link_target] |
714 | + add_to[canonical_url(link_target)] = sheets |
715 | + return add_to |
716 | + |
717 | + |
718 | +class TestTranslationLinksAggregator(TestCaseWithFactory): |
719 | + """Test `TranslationLinksAggregator`.""" |
720 | + |
721 | + layer = LaunchpadZopelessLayer |
722 | + |
723 | + def setUp(self): |
724 | + super(TestTranslationLinksAggregator, self).setUp() |
725 | + self.aggregator = DumbAggregator() |
726 | + |
727 | + def test_circumscribe_single_pofile(self): |
728 | + # If passed a single POFile, _circumscribe returns a list of |
729 | + # just that POFile. |
730 | + pofile = self.factory.makePOFile(language_code='lua') |
731 | + |
732 | + links = self.aggregator._circumscribe([pofile]) |
733 | + |
734 | + self.assertEqual(map_link(pofile), links) |
735 | + |
736 | + def test_circumscribe_product_wild_mix(self): |
737 | + # A combination of wildly different POFiles in the same product |
738 | + # yields links to the individual POFiles. |
739 | + pofile1 = self.factory.makePOFile(language_code='sux') |
740 | + product = pofile1.potemplate.productseries.product |
741 | + series2 = self.factory.makeProductSeries(product) |
742 | + template2 = self.factory.makePOTemplate(productseries=series2) |
743 | + pofile2 = self.factory.makePOFile( |
744 | + potemplate=template2, language_code='la') |
745 | + |
746 | + links = self.aggregator._circumscribe([pofile1, pofile2]) |
747 | + |
748 | + expected_links = map_link(pofile1) |
749 | + expected_links = map_link(pofile2, add_to=expected_links) |
750 | + self.assertEqual(expected_links, links) |
751 | + |
752 | + def test_circumscribe_different_templates(self): |
753 | + # A combination of POFiles in the same language but different |
754 | + # templates of the same productseries is represented as a link |
755 | + # to the ProductSeriesLanguage. |
756 | + pofile1 = self.factory.makePOFile(language_code='nl') |
757 | + series = pofile1.potemplate.productseries |
758 | + template2 = self.factory.makePOTemplate(productseries=series) |
759 | + pofile2 = self.factory.makePOFile( |
760 | + potemplate=template2, language_code='nl') |
761 | + |
762 | + links = self.aggregator._circumscribe([pofile1, pofile2]) |
763 | + |
764 | + psl = ProductSeriesLanguage(series, pofile1.language) |
765 | + self.assertEqual(map_link(psl, [pofile1, pofile2]), links) |
766 | + |
767 | + def test_circumscribe_different_languages(self): |
768 | + # If the POFiles differ only in language, we get a link to the |
769 | + # overview for the template. |
770 | + pofile1 = self.factory.makePOFile(language_code='nl') |
771 | + template = pofile1.potemplate |
772 | + pofile2 = self.factory.makePOFile( |
773 | + potemplate=template, language_code='lo') |
774 | + |
775 | + pofiles = [pofile1, pofile2] |
776 | + links = self.aggregator._circumscribe(pofiles) |
777 | + |
778 | + self.assertEqual(map_link(template, pofiles), links) |
779 | + |
780 | + def test_circumscribe_sharing_pofiles(self): |
781 | + # In a Product, two POFiles may share their translations. For |
782 | + # now, we link to each individually. We may want to make this |
783 | + # more clever in the future. |
784 | + pofile1 = self.factory.makePOFile(language_code='nl') |
785 | + template1 = pofile1.potemplate |
786 | + series1 = template1.productseries |
787 | + series2 = self.factory.makeProductSeries(product=series1.product) |
788 | + template2 = self.factory.makePOTemplate( |
789 | + productseries=series2, name=template1.name, |
790 | + translation_domain=template1.translation_domain) |
791 | + pofile2 = template2.getPOFileByLang('nl') |
792 | + |
793 | + pofiles = [pofile1, pofile2] |
794 | + links = self.aggregator._circumscribe(pofiles) |
795 | + |
796 | + expected_links = map_link(pofile1) |
797 | + expected_links = map_link(pofile2, add_to=expected_links) |
798 | + self.assertEqual(expected_links, links) |
799 | + |
800 | + def test_circumscribe_package_different_languages(self): |
801 | + # For package POFiles in the same template but different |
802 | + # languages, we link to the template. |
803 | + package = self.factory.makeSourcePackage() |
804 | + package.distroseries.distribution.official_rosetta = True |
805 | + template = self.factory.makePOTemplate( |
806 | + distroseries=package.distroseries, |
807 | + sourcepackagename=package.sourcepackagename) |
808 | + pofile1 = self.factory.makePOFile( |
809 | + potemplate=template, language_code='nl') |
810 | + pofile2 = self.factory.makePOFile( |
811 | + potemplate=template, language_code='ka') |
812 | + |
813 | + pofiles = [pofile1, pofile2] |
814 | + links = self.aggregator._circumscribe(pofiles) |
815 | + self.assertEqual(map_link(template, pofiles), links) |
816 | + |
817 | + def test_circumscribe_package_different_templates(self): |
818 | + # For package POFiles in different templates, we to the |
819 | + # package's template list. There is no "source package series |
820 | + # language" page. |
821 | + package = self.factory.makeSourcePackage() |
822 | + package.distroseries.distribution.official_rosetta = True |
823 | + template1 = self.factory.makePOTemplate( |
824 | + distroseries=package.distroseries, |
825 | + sourcepackagename=package.sourcepackagename) |
826 | + template2 = self.factory.makePOTemplate( |
827 | + distroseries=package.distroseries, |
828 | + sourcepackagename=package.sourcepackagename) |
829 | + pofile1 = self.factory.makePOFile( |
830 | + potemplate=template1, language_code='nl') |
831 | + pofile2 = self.factory.makePOFile( |
832 | + potemplate=template2, language_code='nl') |
833 | + |
834 | + pofiles = [pofile1, pofile2] |
835 | + links = self.aggregator._circumscribe(pofiles) |
836 | + |
837 | + self.assertEqual(map_link(package, pofiles), links) |
838 | + |
839 | + def test_circumscribe_pofile_plus_template(self): |
840 | + # A template circumscribes both itself and any of its |
841 | + # translations. |
842 | + pofile = self.factory.makePOFile(language_code='uga') |
843 | + template = pofile.potemplate |
844 | + |
845 | + sheets = [pofile, template] |
846 | + links = self.aggregator._circumscribe(sheets) |
847 | + |
848 | + self.assertEqual(map_link(template, sheets), links) |
849 | + |
850 | + def test_aggregate(self): |
851 | + # The aggregator represents a series of POFiles as a series of |
852 | + # target descriptions, aggregating where possible. |
853 | + |
854 | + # Trivial case: no POFiles means no targets. |
855 | + self.assertEqual([], self.aggregator.aggregate([])) |
856 | + |
857 | + # Basic case: one POFile yields its product or package. |
858 | + pofile = self.factory.makePOFile(language_code='ca') |
859 | + product = pofile.potemplate.productseries.product |
860 | + |
861 | + descriptions = self.aggregator.aggregate([pofile]) |
862 | + |
863 | + expected = [(product, canonical_url(pofile), [pofile])] |
864 | + self.assertEqual(expected, descriptions) |
865 | + |
866 | + def test_aggregate_potemplate(self): |
867 | + # Besides POFiles, you can also feed an aggregator POTemplates. |
868 | + template = self.factory.makePOTemplate() |
869 | + product = template.productseries.product |
870 | + |
871 | + descriptions = self.aggregator.aggregate([template]) |
872 | + |
873 | + expected = [(product, canonical_url(template), [template])] |
874 | + self.assertEqual(expected, descriptions) |
875 | + |
876 | + def test_aggregate_product_and_package(self): |
877 | + # The aggregator keeps a product and a package separate. |
878 | + product_pofile = self.factory.makePOFile(language_code='th') |
879 | + product = product_pofile.potemplate.productseries.product |
880 | + removeSecurityProxy(product_pofile).unreviewed_count = 1 |
881 | + |
882 | + package = self.factory.makeSourcePackage() |
883 | + package.distroseries.distribution.official_rosetta = True |
884 | + package_template = self.factory.makePOTemplate( |
885 | + distroseries=package.distroseries, |
886 | + sourcepackagename=package.sourcepackagename) |
887 | + package_pofile = self.factory.makePOFile( |
888 | + potemplate=package_template, language_code='th') |
889 | + removeSecurityProxy(package_pofile).unreviewed_count = 2 |
890 | + |
891 | + descriptions = self.aggregator.aggregate( |
892 | + [product_pofile, package_pofile]) |
893 | + |
894 | + expected = [ |
895 | + (product, canonical_url(product_pofile), [product_pofile]), |
896 | + (package, canonical_url(package_pofile), [package_pofile]), |
897 | + ] |
898 | + self.assertEqual(expected, descriptions) |
899 | + |
900 | + def test_aggregate_bundles_productseries(self): |
901 | + # _aggregateTranslationTargets describes POFiles for the same |
902 | + # ProductSeries together. |
903 | + pofile1 = self.factory.makePOFile(language_code='es') |
904 | + series = pofile1.potemplate.productseries |
905 | + template2 = self.factory.makePOTemplate(productseries=series) |
906 | + pofile2 = self.factory.makePOFile( |
907 | + language_code='br', potemplate=template2) |
908 | + |
909 | + pofiles = [pofile1, pofile2] |
910 | + descriptions = self.aggregator.aggregate(pofiles) |
911 | + |
912 | + self.assertEqual(1, len(descriptions)) |
913 | + self.assertEqual( |
914 | + [(series.product, canonical_url(series), pofiles)], descriptions) |
915 | + |
916 | + def test_aggregate_bundles_package(self): |
917 | + # _aggregateTranslationTargets describes POFiles for the same |
918 | + # ProductSeries together. |
919 | + package = self.factory.makeSourcePackage() |
920 | + package.distroseries.distribution.official_rosetta = True |
921 | + template1 = self.factory.makePOTemplate( |
922 | + distroseries=package.distroseries, |
923 | + sourcepackagename=package.sourcepackagename) |
924 | + pofile1 = self.factory.makePOFile( |
925 | + language_code='es', potemplate=template1) |
926 | + template2 = self.factory.makePOTemplate( |
927 | + distroseries=package.distroseries, |
928 | + sourcepackagename=package.sourcepackagename) |
929 | + pofile2 = self.factory.makePOFile( |
930 | + language_code='br', potemplate=template2) |
931 | + |
932 | + pofiles = [pofile1, pofile2] |
933 | + descriptions = self.aggregator.aggregate(pofiles) |
934 | + |
935 | + expected = [(package, canonical_url(package), pofiles)] |
936 | + self.assertEqual(expected, descriptions) |
937 | + |
938 | + |
939 | +def test_suite(): |
940 | + return TestLoader().loadTestsFromName(__name__) |
941 | |
942 | === added file 'lib/lp/translations/browser/translationlinksaggregator.py' |
943 | --- lib/lp/translations/browser/translationlinksaggregator.py 1970-01-01 00:00:00 +0000 |
944 | +++ lib/lp/translations/browser/translationlinksaggregator.py 2009-08-25 10:26:28 +0000 |
945 | @@ -0,0 +1,186 @@ |
946 | +# Copyright 2009 Canonical Ltd. This software is licensed under the |
947 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
948 | + |
949 | +__metaclass__ = type |
950 | + |
951 | +__all__ = [ |
952 | + 'TranslationLinksAggregator', |
953 | + ] |
954 | + |
955 | +from canonical.launchpad.webapp import canonical_url |
956 | +from lp.translations.interfaces.pofile import IPOFile |
957 | +from lp.translations.model.productserieslanguage import ProductSeriesLanguage |
958 | + |
959 | + |
960 | +class TranslationLinksAggregator: |
961 | + """Aggregate `POFile`s and/or `POTemplate`s into meaningful targets. |
962 | + |
963 | + Here, `POFile`s and `POTemplate`s are referred to collectively as |
964 | + "sheets." |
965 | + """ |
966 | + |
967 | + # Suffix to append to URL when linking to a POFile. |
968 | + pofile_link_suffix = '' |
969 | + |
970 | + def describe(self, target, link, covered_sheets): |
971 | + """Overridable: return description of given translations link. |
972 | + |
973 | + :param target: `Product` or `SourcePackage`. |
974 | + :param link: URL linking to `covered_sheets` in the UI. |
975 | + :param covered_sheets: `POFile`s and/or `POTemplate`s being |
976 | + linked and described together. |
977 | + :return: Some description that will get added to a list and |
978 | + returned by `aggregate`. |
979 | + """ |
980 | + raise NotImplementedError() |
981 | + |
982 | + def _bundle(self, sheets): |
983 | + """Bundle `sheets` based on target: `Product` or `SourcePackage`. |
984 | + |
985 | + :param sheets: Sequence of `POFile`s and/or `POTemplate`s. |
986 | + :return: Dict mapping each targets to a list representing its |
987 | + `POFile`s and `POTemplate`s as found in `sheets`. |
988 | + """ |
989 | + targets = {} |
990 | + for sheet in sheets: |
991 | + if IPOFile.providedBy(sheet): |
992 | + template = sheet.potemplate |
993 | + else: |
994 | + template = sheet |
995 | + |
996 | + if template.productseries: |
997 | + target = template.productseries.product |
998 | + else: |
999 | + distroseries = template.distroseries |
1000 | + sourcepackagename = template.sourcepackagename |
1001 | + target = distroseries.getSourcePackage(sourcepackagename) |
1002 | + |
1003 | + if target not in targets: |
1004 | + targets[target] = [] |
1005 | + |
1006 | + targets[target].append(sheet) |
1007 | + |
1008 | + return targets |
1009 | + |
1010 | + def _composeLink(self, sheet): |
1011 | + """Produce a link to a `POFile` or `POTemplate`.""" |
1012 | + link = canonical_url(sheet) |
1013 | + if IPOFile.providedBy(sheet): |
1014 | + link += self.pofile_link_suffix |
1015 | + |
1016 | + return link |
1017 | + |
1018 | + def _getTemplate(self, sheet): |
1019 | + """Return `POTemplate` for `sheet`. |
1020 | + |
1021 | + :param sheet: A `POTemplate` or `POFile`. |
1022 | + """ |
1023 | + if IPOFile.providedBy(sheet): |
1024 | + return sheet.potemplate |
1025 | + else: |
1026 | + return sheet |
1027 | + |
1028 | + def _getLanguage(self, sheet): |
1029 | + """Return language `sheet` is in, if `sheet` is an `IPOFile`.""" |
1030 | + if IPOFile.providedBy(sheet): |
1031 | + return sheet.language |
1032 | + else: |
1033 | + return None |
1034 | + |
1035 | + def _countLanguages(self, sheets): |
1036 | + """Count languages among `sheets`. |
1037 | + |
1038 | + A template's language is None, which also counts. |
1039 | + """ |
1040 | + return len(set(self._getLanguage(sheet) for sheet in sheets)) |
1041 | + |
1042 | + def _circumscribe(self, sheets): |
1043 | + """Find the best common UI link to cover all of `sheets`. |
1044 | + |
1045 | + :param sheets: List of `POFile`s and/or `POTemplate`s. |
1046 | + :return: Dict containing a set of links and the respective lists |
1047 | + of `sheets` they cover. |
1048 | + """ |
1049 | + first_sheet = sheets[0] |
1050 | + if len(sheets) == 1: |
1051 | + # Simple case: one sheet. |
1052 | + return {self._composeLink(first_sheet): sheets} |
1053 | + |
1054 | + templates = set([self._getTemplate(sheet) for sheet in sheets]) |
1055 | + |
1056 | + productseries = set( |
1057 | + template.productseries |
1058 | + for template in templates |
1059 | + if template.productseries) |
1060 | + |
1061 | + products = set(series.product for series in productseries) |
1062 | + |
1063 | + sourcepackagenames = set( |
1064 | + template.sourcepackagename |
1065 | + for template in templates |
1066 | + if template.sourcepackagename) |
1067 | + |
1068 | + distroseries = set( |
1069 | + template.distroseries |
1070 | + for template in templates |
1071 | + if template.sourcepackagename) |
1072 | + |
1073 | + assert len(products) <= 1, "Got more than one product." |
1074 | + assert len(sourcepackagenames) <= 1, "Got more than one package." |
1075 | + assert len(distroseries) <= 1, "Got more than one distroseries." |
1076 | + assert len(products) + len(sourcepackagenames) == 1, ( |
1077 | + "Didn't get exactly one product or one package.") |
1078 | + |
1079 | + first_template = self._getTemplate(first_sheet) |
1080 | + |
1081 | + if len(templates) == 1: |
1082 | + # Multiple inputs, but all for the same template. Link to |
1083 | + # the template. |
1084 | + return {self._composeLink(first_template): sheets} |
1085 | + |
1086 | + if sourcepackagenames: |
1087 | + # Multiple inputs, but they have to be all for the same |
1088 | + # source package. Show its template listing. |
1089 | + distroseries = first_template.distroseries |
1090 | + packagename = first_template.sourcepackagename |
1091 | + link = canonical_url(distroseries.getSourcePackage(packagename)) |
1092 | + return {link: sheets} |
1093 | + |
1094 | + if len(productseries) == 1: |
1095 | + # All for the same ProductSeries. |
1096 | + series = first_template.productseries |
1097 | + if self._countLanguages(sheets) == 1: |
1098 | + # All for the same language in the same ProductSeries, |
1099 | + # though still for different templates. Link to |
1100 | + # ProductSeriesLanguage. |
1101 | + productserieslanguage = ProductSeriesLanguage( |
1102 | + series, first_sheet.language) |
1103 | + return {canonical_url(productserieslanguage): sheets} |
1104 | + else: |
1105 | + # Multiple templates and languages in the same product |
1106 | + # series, or a mix of templates and at least one |
1107 | + # language. Show the product series' templates listing. |
1108 | + return {canonical_url(series): sheets} |
1109 | + |
1110 | + # Different release series of the same product. Break down into |
1111 | + # individual sheets. We could try recursing here to get a better |
1112 | + # set of aggregated links, but may not be worth the trouble. |
1113 | + return dict( |
1114 | + (self._composeLink(sheet), [sheet]) for sheet in sheets) |
1115 | + |
1116 | + def aggregate(self, sheets): |
1117 | + """Aggregate `sheets` into a list of translation target descriptions. |
1118 | + |
1119 | + Targets are aggregated into "sensible" chunks first.c |
1120 | + |
1121 | + :return: A list of whatever the implementation for `describe` |
1122 | + returns for the sensible chunks. |
1123 | + """ |
1124 | + links = [] |
1125 | + for target, sheets in self._bundle(sheets).iteritems(): |
1126 | + assert sheets, "Translation target has no POFiles or templates." |
1127 | + links_and_sheets = self._circumscribe(sheets) |
1128 | + for link, covered_sheets in links_and_sheets.iteritems(): |
1129 | + links.append(self.describe(target, link, covered_sheets)) |
1130 | + |
1131 | + return links |
= Translation Links Aggregator =
This branch is a bit overweight because it lifts code & tests out of a
view class and into a module of its own. It's preparatory work for the
"stuff to translate" addition to the Translations personal dashboard.
A recurring problem is this one: given a bunch of POFiles, what is a
sensible set of links to present them to the user? One extreme answer
would be to link to (and describe) each individually, but we're trying
to build a more sensible UI with less pointless information for the
viewer to plod through.
At the other extreme, we could link to the largest single thing in
Launchpad that represents all of the translations: POFile; POTemplate;
ProductSeries or SourcePackage; Product or Distribution; or even all of
Launchpad Translations. The more extreme it gets, the less sensible it
becomes. We like to avoid "false positives" where lots of the stuff we
link to is not actually in the original list of translations.
The TranslationLink sAggregator, introduced here, tries to find a happy
medium. It breaks your set of translations down by ProductSeries or
SourcePackage, and within each of those, it tries to find a good common
link to represent all the inputs without too many false positives. If
it finds no really good single link, it links to the individual POFiles.
You use this new class by inheriting from it and overloading "describe."
This method receives all relevant information about a translation target
and turns it into a description suitable for use by the UI. The
description can take whatever form the subclass likes, making the
aggregator fairly flexible.
(Description is the very last stage of aggregation and I could have left
it out. I could have made the aggregator produce a sequence of tuples,
each with the arguments that now are passed to the describe method. The
caller could then process each of these in any way desired. But in my
experience passing lists of tuples around soon affects maintainability;
turning them into classes on the other hand leads to obesity).
As a free extra, the aggregator supports not just POFiles but
POTemplates as well. In some ways they are like "POFiles without a
language." Danilo reviewed the branch that first introduced this stuff
and found that it solved a very similar problem to one he faced with
translation exports. One of the main differences was that in his case
there might be templates as well as translations. It was easy enough to
add, and hopefully this means that the class will serve his needs as
well.
The hard part about this branch is that it breaks the unit tests for the onView into two: a new one for the aggregator, and the
PersonTranslati
gutted remains of the view test proper. Some detail in the unit-testing
will be lost, and a bit of additional high-level testing makes up for
it.
== Tests, demo & Q/A ==
Test with: onview saggregator
{{{
./bin/test -vv -t persontranslati
./bin/test -vv -t translationlink
}}}
To demo and Q/A, look at the home page for someone who is in a
translation group, under the Translations tab. If that person is you,
you'll see a list of translations suggested for review. That list will
now be aggregated using t...