Merge lp:~danilo/launchpad/translatedlanguage into lp:launchpad
- translatedlanguage
- Merge into devel
Status: | Merged | ||||
---|---|---|---|---|---|
Merged at revision: | 11253 | ||||
Proposed branch: | lp:~danilo/launchpad/translatedlanguage | ||||
Merge into: | lp:launchpad | ||||
Diff against target: |
1251 lines (+803/-158) 15 files modified
lib/lp/registry/model/productseries.py (+15/-19) lib/lp/testing/factory.py (+2/-2) lib/lp/translations/browser/configure.zcml (+1/-1) lib/lp/translations/browser/productseries.py (+2/-0) lib/lp/translations/browser/serieslanguage.py (+11/-8) lib/lp/translations/configure.zcml (+17/-0) lib/lp/translations/interfaces/potemplate.py (+14/-0) lib/lp/translations/interfaces/productserieslanguage.py (+4/-29) lib/lp/translations/interfaces/translatedlanguage.py (+79/-0) lib/lp/translations/model/potemplate.py (+10/-3) lib/lp/translations/model/productserieslanguage.py (+20/-83) lib/lp/translations/model/translatedlanguage.py (+132/-0) lib/lp/translations/tests/test_productserieslanguage.py (+15/-13) lib/lp/translations/tests/test_translatedlanguage.py (+462/-0) lib/lp/translations/tests/test_translationtemplatescollection.py (+19/-0) |
||||
To merge this branch: | bzr merge lp:~danilo/launchpad/translatedlanguage | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Māris Fogels (community) | code | Approve | |
Review via email: mp+30788@code.launchpad.net |
Commit message
Provide ITranslatedLanguage interface along with a TranslatedLangu
Description of the change
= ITranslatedLanguage =
This provides a generic ITranslatedLanguage interface for objects which are a translation of something (i.e. a productseries, distroseries, sourcepackage, template) into a single language, along with a mixin that implements this interface in a generic way.
Mixin is to replace most of the model code on DistroSeriesLan
The next steps would be to switch ProductSeriesLa
As a preparation for getting rid of IRosettaStats, I introduce a temporary statistics object implementation (a dict) which we want to switch everything to (a better one is in progress in one of Adi's branches).
The most interesting bit of the code is inside the mixin: POFilesByPOTemp
Unfortunately, for listifying TranslatedLangu
It is (somewhat) indirectly unit-tested inside the TranslationTemp
Full test is otherwise written in a way to make it easy to extend for testing over different types of objects implementing ITranslatedLang
= Tests =
bin/test -cvvt test_translated
= Demo & QA =
A few examples:
https:/
https:/
https:/
https:/
And to confirm we haven't broken DistroSeriesLan
https:/
https:/
= Launchpad lint =
Checking for conflicts and issues in changed files.
Linting changed files:
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
./lib/lp/
736: E301 expected 1 blank line, found 2
750: E301 expected 1 blank line, found 2
784: E302 expected 2 blank lines, found 1
1312: E202 whitespace before ']'
1400: E202 whitespace before ']'
1407: E202 whitespace before ']'
1510: E202 whitespace before ']'
(E301 happens due to comments in interface definition, E202 because of multi-line list definitions; I am not changing these for now, though I did fix a bunch of lint issues; also, these are across two different files: interfaces/
Данило Шеган (danilo) wrote : | # |
Hi Maris,
Thanks a lot for reviewing this over-sized branch!
У уто, 27. 07 2010. у 21:34 +0000, Māris Fogels пише:
> The code looks OK for this branch. I like the object redesign so far. The branch is actually too large for me to review well, but I tried anyway. Here is what I saw:
>
> • I'm scared by code that passes around large argument lists like
> ProductSeriesLa
> reference to the pofile in it's constructor, is it possible to have
> PSL pull the counts from the pofile itself internally? Other options
> are to create a new method PSL.setCountsFr
> object like PSLCounts(), or just pass in the calculated value:
> PSL.setCounts(
Not really: PSL will sometimes have only a single PO file (well,
interestingly, *most* of the time), but a generic case allows it to have
more than one. And for all the stats that we show, we need all these
numbers.
The reason for it's existence is that you sometimes don't want to do a
single query for each of the languages you are fetching (eg.
https:/
aggregated stats from a bunch of stats for ~100 pofiles per language
with one very fast query), so we basically allow "outer" objects to
initialize PSL. When we switch to a stats object itself, we'd be
setting each of them directly, so we won't be passing large arguments
list like this.
> • interfaces/
Fixed.
> • The test in test_TwoTemplat
> easily in a follow-up branch. That one test method has at least two,
> if not three, complete tests contained within it.
Actually, it also does a lot of assertions that are not needed for that
particular test. Like asserting behaviour that was already tested in
other tests. I've removed those.
I've split it into three tests first, and then actually got rid of the
two that are already tested in test_translated
(test_pofiles_
The only one left is the one testing the single-POFile optimization
case.
So, thanks to you pointing this out, I've cleaned these tests so we
don't get them repeated between interfaces. It is a separate branch
though (lp:~danilo/launchpad/psl-tests-cleanup).
> • I found the test names in TestTranslatedL
> For example, "test_parent" looks something like
> "test_psl_
test_parent just tests that parent is correctly initialized for a new
object. The naming is directly linked to the attribute being tested. I
name unit tests like that (test_"attribute" or test_"method name", and
then add additional descriptors after that when needed).
> , and I can't tell from the tests
> alone if the test_recalculat
> correctly or not (maybe the Counts tests will be clearer when they are
> rewritten to test the new Stats object instead of the Mixin).
These tests are not extensive, and they are going to be hard to figure
out as long as we used the messed-up RosettaStats interface (which
stores values w...
Preview Diff
1 | === modified file 'lib/lp/registry/model/productseries.py' | |||
2 | --- lib/lp/registry/model/productseries.py 2010-07-15 15:01:18 +0000 | |||
3 | +++ lib/lp/registry/model/productseries.py 2010-07-28 22:36:12 +0000 | |||
4 | @@ -465,12 +465,15 @@ | |||
5 | 465 | 465 | ||
6 | 466 | for language, pofile in ordered_results: | 466 | for language, pofile in ordered_results: |
7 | 467 | psl = ProductSeriesLanguage(self, language, pofile=pofile) | 467 | psl = ProductSeriesLanguage(self, language, pofile=pofile) |
14 | 468 | psl.setCounts(pofile.potemplate.messageCount(), | 468 | total = pofile.potemplate.messageCount() |
15 | 469 | pofile.currentCount(), | 469 | imported = pofile.currentCount() |
16 | 470 | pofile.updatesCount(), | 470 | changed = pofile.updatesCount() |
17 | 471 | pofile.rosettaCount(), | 471 | rosetta = pofile.rosettaCount() |
18 | 472 | pofile.unreviewedCount(), | 472 | unreviewed = pofile.unreviewedCount() |
19 | 473 | pofile.date_changed) | 473 | translated = imported + rosetta |
20 | 474 | new = rosetta - changed | ||
21 | 475 | psl.setCounts(total, translated, new, changed, unreviewed) | ||
22 | 476 | psl.last_changed_date = pofile.date_changed | ||
23 | 474 | results.append(psl) | 477 | results.append(psl) |
24 | 475 | else: | 478 | else: |
25 | 476 | # If there is more than one template, do a single | 479 | # If there is more than one template, do a single |
26 | @@ -498,22 +501,15 @@ | |||
27 | 498 | POTemplate.iscurrent==True, | 501 | POTemplate.iscurrent==True, |
28 | 499 | Language.id!=english.id).group_by(Language) | 502 | Language.id!=english.id).group_by(Language) |
29 | 500 | 503 | ||
30 | 501 | # XXX: Ursinha 2009-11-02: The Max(POFile.date_changed) result | ||
31 | 502 | # here is a naive datetime. My guess is that it happens | ||
32 | 503 | # because UTC awareness is attibuted to the field in the POFile | ||
33 | 504 | # model class, and in this case the Max function deals directly | ||
34 | 505 | # with the value returned from the database without | ||
35 | 506 | # instantiating it. | ||
36 | 507 | # This seems to be irrelevant to what we're trying to achieve | ||
37 | 508 | # here, but making a note either way. | ||
38 | 509 | |||
39 | 510 | ordered_results = query.order_by(['Language.englishname']) | 504 | ordered_results = query.order_by(['Language.englishname']) |
40 | 511 | 505 | ||
43 | 512 | for (language, imported, changed, new, unreviewed, | 506 | for (language, imported, changed, rosetta, unreviewed, |
44 | 513 | last_changed) in ordered_results: | 507 | last_changed) in ordered_results: |
45 | 514 | psl = ProductSeriesLanguage(self, language) | 508 | psl = ProductSeriesLanguage(self, language) |
48 | 515 | psl.setCounts( | 509 | translated = imported + rosetta |
49 | 516 | total, imported, changed, new, unreviewed, last_changed) | 510 | new = rosetta - changed |
50 | 511 | psl.setCounts(total, translated, new, changed, unreviewed) | ||
51 | 512 | psl.last_changed_date = last_changed | ||
52 | 517 | results.append(psl) | 513 | results.append(psl) |
53 | 518 | 514 | ||
54 | 519 | return results | 515 | return results |
55 | 520 | 516 | ||
56 | === modified file 'lib/lp/testing/factory.py' | |||
57 | --- lib/lp/testing/factory.py 2010-07-28 19:49:46 +0000 | |||
58 | +++ lib/lp/testing/factory.py 2010-07-28 22:36:12 +0000 | |||
59 | @@ -160,7 +160,6 @@ | |||
60 | 160 | ANONYMOUS, | 160 | ANONYMOUS, |
61 | 161 | login, | 161 | login, |
62 | 162 | login_as, | 162 | login_as, |
63 | 163 | logout, | ||
64 | 164 | run_with_login, | 163 | run_with_login, |
65 | 165 | temp_dir, | 164 | temp_dir, |
66 | 166 | time_counter, | 165 | time_counter, |
67 | @@ -846,7 +845,7 @@ | |||
68 | 846 | url = self.getUniqueURL() | 845 | url = self.getUniqueURL() |
69 | 847 | else: | 846 | else: |
70 | 848 | raise UnknownBranchTypeError( | 847 | raise UnknownBranchTypeError( |
72 | 849 | 'Unrecognized branch type: %r' % (branch_type,)) | 848 | 'Unrecognized branch type: %r' % (branch_type, )) |
73 | 850 | 849 | ||
74 | 851 | namespace = get_branch_namespace( | 850 | namespace = get_branch_namespace( |
75 | 852 | owner, product=product, distroseries=distroseries, | 851 | owner, product=product, distroseries=distroseries, |
76 | @@ -1633,6 +1632,7 @@ | |||
77 | 1633 | return series | 1632 | return series |
78 | 1634 | 1633 | ||
79 | 1635 | def makeLanguage(self, language_code=None, name=None): | 1634 | def makeLanguage(self, language_code=None, name=None): |
80 | 1635 | """Makes a language given the language_code and name.""" | ||
81 | 1636 | if language_code is None: | 1636 | if language_code is None: |
82 | 1637 | language_code = self.getUniqueString('lang') | 1637 | language_code = self.getUniqueString('lang') |
83 | 1638 | if name is None: | 1638 | if name is None: |
84 | 1639 | 1639 | ||
85 | === modified file 'lib/lp/translations/browser/configure.zcml' | |||
86 | --- lib/lp/translations/browser/configure.zcml 2010-07-16 16:58:55 +0000 | |||
87 | +++ lib/lp/translations/browser/configure.zcml 2010-07-28 22:36:12 +0000 | |||
88 | @@ -269,7 +269,7 @@ | |||
89 | 269 | <browser:url | 269 | <browser:url |
90 | 270 | for="lp.translations.interfaces.productserieslanguage.IProductSeriesLanguage" | 270 | for="lp.translations.interfaces.productserieslanguage.IProductSeriesLanguage" |
91 | 271 | path_expression="string:+lang/${language/code}" | 271 | path_expression="string:+lang/${language/code}" |
93 | 272 | attribute_to_parent="productseries" | 272 | attribute_to_parent="parent" |
94 | 273 | rootsite="translations"/> | 273 | rootsite="translations"/> |
95 | 274 | <browser:navigation | 274 | <browser:navigation |
96 | 275 | module="lp.translations.browser.serieslanguage" | 275 | module="lp.translations.browser.serieslanguage" |
97 | 276 | 276 | ||
98 | === modified file 'lib/lp/translations/browser/productseries.py' | |||
99 | --- lib/lp/translations/browser/productseries.py 2010-07-22 14:59:48 +0000 | |||
100 | +++ lib/lp/translations/browser/productseries.py 2010-07-28 22:36:12 +0000 | |||
101 | @@ -370,10 +370,12 @@ | |||
102 | 370 | productserieslang = ( | 370 | productserieslang = ( |
103 | 371 | productserieslangset.getProductSeriesLanguage( | 371 | productserieslangset.getProductSeriesLanguage( |
104 | 372 | self.context, lang, pofile=pofile)) | 372 | self.context, lang, pofile=pofile)) |
105 | 373 | productserieslang.recalculateCounts() | ||
106 | 373 | else: | 374 | else: |
107 | 374 | productserieslang = ( | 375 | productserieslang = ( |
108 | 375 | productserieslangset.getProductSeriesLanguage( | 376 | productserieslangset.getProductSeriesLanguage( |
109 | 376 | self.context, lang)) | 377 | self.context, lang)) |
110 | 378 | productserieslang.recalculateCounts() | ||
111 | 377 | productserieslangs.append( | 379 | productserieslangs.append( |
112 | 378 | productserieslang) | 380 | productserieslang) |
113 | 379 | 381 | ||
114 | 380 | 382 | ||
115 | === modified file 'lib/lp/translations/browser/serieslanguage.py' | |||
116 | --- lib/lp/translations/browser/serieslanguage.py 2010-03-04 07:31:38 +0000 | |||
117 | +++ lib/lp/translations/browser/serieslanguage.py 2010-07-28 22:36:12 +0000 | |||
118 | @@ -29,7 +29,7 @@ | |||
119 | 29 | 29 | ||
120 | 30 | 30 | ||
121 | 31 | class BaseSeriesLanguageView(LaunchpadView): | 31 | class BaseSeriesLanguageView(LaunchpadView): |
123 | 32 | """View base class to render translation status for an | 32 | """View base class to render translation status for an |
124 | 33 | `IDistroSeries` and `IProductSeries` | 33 | `IDistroSeries` and `IProductSeries` |
125 | 34 | 34 | ||
126 | 35 | This class should not be directly instantiated. | 35 | This class should not be directly instantiated. |
127 | @@ -46,12 +46,15 @@ | |||
128 | 46 | self.translationgroup = translationgroup | 46 | self.translationgroup = translationgroup |
129 | 47 | self.form = self.request.form | 47 | self.form = self.request.form |
130 | 48 | 48 | ||
137 | 49 | self.batchnav = BatchNavigator( | 49 | if IDistroSeriesLanguage.providedBy(self.context): |
138 | 50 | self.series.getCurrentTranslationTemplates(), | 50 | self.batchnav = BatchNavigator( |
139 | 51 | self.request) | 51 | self.series.getCurrentTranslationTemplates(), |
140 | 52 | 52 | self.request) | |
141 | 53 | self.pofiles = self.context.getPOFilesFor( | 53 | self.pofiles = self.context.getPOFilesFor( |
142 | 54 | self.batchnav.currentBatch()) | 54 | self.batchnav.currentBatch()) |
143 | 55 | else: | ||
144 | 56 | self.batchnav = BatchNavigator(self.context.pofiles, self.request) | ||
145 | 57 | self.pofiles = self.batchnav.currentBatch() | ||
146 | 55 | 58 | ||
147 | 56 | @property | 59 | @property |
148 | 57 | def translation_group(self): | 60 | def translation_group(self): |
149 | @@ -77,7 +80,7 @@ | |||
150 | 77 | @property | 80 | @property |
151 | 78 | def access_level_description(self): | 81 | def access_level_description(self): |
152 | 79 | """Must not be called when there's no translation group.""" | 82 | """Must not be called when there's no translation group.""" |
154 | 80 | 83 | ||
155 | 81 | if is_read_only(): | 84 | if is_read_only(): |
156 | 82 | return ( | 85 | return ( |
157 | 83 | "No work can be done on these translations while Launchpad " | 86 | "No work can be done on these translations while Launchpad " |
158 | 84 | 87 | ||
159 | === modified file 'lib/lp/translations/configure.zcml' | |||
160 | --- lib/lp/translations/configure.zcml 2010-07-22 02:41:43 +0000 | |||
161 | +++ lib/lp/translations/configure.zcml 2010-07-28 22:36:12 +0000 | |||
162 | @@ -399,6 +399,16 @@ | |||
163 | 399 | interface="lp.translations.interfaces.productserieslanguage.IProductSeriesLanguageSet"/> | 399 | interface="lp.translations.interfaces.productserieslanguage.IProductSeriesLanguageSet"/> |
164 | 400 | </securedutility> | 400 | </securedutility> |
165 | 401 | 401 | ||
166 | 402 | <!-- TranslatedLanguage --> | ||
167 | 403 | <facet | ||
168 | 404 | facet="translations"> | ||
169 | 405 | <class | ||
170 | 406 | class="lp.translations.model.translatedlanguage.POFilesByPOTemplates"> | ||
171 | 407 | <allow | ||
172 | 408 | interface="lp.translations.interfaces.translatedlanguage.IPOFilesByPOTemplates"/> | ||
173 | 409 | </class> | ||
174 | 410 | </facet> | ||
175 | 411 | |||
176 | 402 | <!-- POTemplate --> | 412 | <!-- POTemplate --> |
177 | 403 | <facet | 413 | <facet |
178 | 404 | facet="translations"> | 414 | facet="translations"> |
179 | @@ -430,6 +440,13 @@ | |||
180 | 430 | provides="lp.translations.interfaces.translationcommonformat.ITranslationFileData" | 440 | provides="lp.translations.interfaces.translationcommonformat.ITranslationFileData" |
181 | 431 | factory="lp.translations.model.potemplate.POTemplateToTranslationFileDataAdapter"/> | 441 | factory="lp.translations.model.potemplate.POTemplateToTranslationFileDataAdapter"/> |
182 | 432 | 442 | ||
183 | 443 | <!-- TranslationTemplatesCollection --> | ||
184 | 444 | <class | ||
185 | 445 | class="lp.translations.model.potemplate.TranslationTemplatesCollection"> | ||
186 | 446 | <allow | ||
187 | 447 | interface="lp.translations.interfaces.potemplate.ITranslationTemplatesCollection"/> | ||
188 | 448 | </class> | ||
189 | 449 | |||
190 | 433 | <!-- POTemplateSet --> | 450 | <!-- POTemplateSet --> |
191 | 434 | 451 | ||
192 | 435 | <securedutility | 452 | <securedutility |
193 | 436 | 453 | ||
194 | === modified file 'lib/lp/translations/interfaces/potemplate.py' | |||
195 | --- lib/lp/translations/interfaces/potemplate.py 2010-07-22 14:59:48 +0000 | |||
196 | +++ lib/lp/translations/interfaces/potemplate.py 2010-07-28 22:36:12 +0000 | |||
197 | @@ -781,5 +781,19 @@ | |||
198 | 781 | exist for it. | 781 | exist for it. |
199 | 782 | """ | 782 | """ |
200 | 783 | 783 | ||
201 | 784 | class ITranslationTemplatesCollection(Interface): | ||
202 | 785 | """A `Collection` of `POTemplate`s.""" | ||
203 | 786 | |||
204 | 787 | def joinOuterPOFile(language=None): | ||
205 | 788 | """Outer-join `POFile` into the collection. | ||
206 | 789 | |||
207 | 790 | :return: A `TranslationTemplatesCollection` with an added outer | ||
208 | 791 | join to `POFile`. | ||
209 | 792 | """ | ||
210 | 793 | |||
211 | 794 | def select(*args): | ||
212 | 795 | """Return a ResultSet for this collection with values set to args.""" | ||
213 | 796 | |||
214 | 797 | |||
215 | 784 | # Monkey patch for circular import avoidance done in | 798 | # Monkey patch for circular import avoidance done in |
216 | 785 | # _schema_circular_imports.py | 799 | # _schema_circular_imports.py |
217 | 786 | 800 | ||
218 | === modified file 'lib/lp/translations/interfaces/productserieslanguage.py' | |||
219 | --- lib/lp/translations/interfaces/productserieslanguage.py 2010-07-19 15:31:57 +0000 | |||
220 | +++ lib/lp/translations/interfaces/productserieslanguage.py 2010-07-28 22:36:12 +0000 | |||
221 | @@ -5,13 +5,13 @@ | |||
222 | 5 | 5 | ||
223 | 6 | from lazr.restful.fields import Reference | 6 | from lazr.restful.fields import Reference |
224 | 7 | 7 | ||
228 | 8 | from zope.interface import Attribute, Interface | 8 | from zope.interface import Interface |
229 | 9 | from zope.schema import ( | 9 | from zope.schema import Choice, TextLine |
227 | 10 | Choice, Datetime, TextLine) | ||
230 | 11 | 10 | ||
231 | 12 | from canonical.launchpad import _ | 11 | from canonical.launchpad import _ |
232 | 13 | from lp.translations.interfaces.pofile import IPOFile | 12 | from lp.translations.interfaces.pofile import IPOFile |
233 | 14 | from lp.translations.interfaces.rosettastats import IRosettaStats | 13 | from lp.translations.interfaces.rosettastats import IRosettaStats |
234 | 14 | from lp.translations.interfaces.translatedlanguage import ITranslatedLanguage | ||
235 | 15 | 15 | ||
236 | 16 | __metaclass__ = type | 16 | __metaclass__ = type |
237 | 17 | 17 | ||
238 | @@ -21,13 +21,9 @@ | |||
239 | 21 | ] | 21 | ] |
240 | 22 | 22 | ||
241 | 23 | 23 | ||
243 | 24 | class IProductSeriesLanguage(IRosettaStats): | 24 | class IProductSeriesLanguage(IRosettaStats, ITranslatedLanguage): |
244 | 25 | """Per-language statistics for a product series.""" | 25 | """Per-language statistics for a product series.""" |
245 | 26 | 26 | ||
246 | 27 | language = Choice( | ||
247 | 28 | title=_('Language to gather statistics for.'), | ||
248 | 29 | vocabulary='Language', required=True, readonly=True) | ||
249 | 30 | |||
250 | 31 | pofile = Reference( | 27 | pofile = Reference( |
251 | 32 | title=_("A POFile if there is only one POTemplate for the series."), | 28 | title=_("A POFile if there is only one POTemplate for the series."), |
252 | 33 | schema=IPOFile, required=False, readonly=True) | 29 | schema=IPOFile, required=False, readonly=True) |
253 | @@ -41,27 +37,6 @@ | |||
254 | 41 | title=_("Title for the per-language per-series page."), | 37 | title=_("Title for the per-language per-series page."), |
255 | 42 | required=False) | 38 | required=False) |
256 | 43 | 39 | ||
257 | 44 | pofiles = Attribute("The set of pofiles in this distroseries for this " | ||
258 | 45 | "language. This includes only the real pofiles where translations " | ||
259 | 46 | "exist.") | ||
260 | 47 | |||
261 | 48 | |||
262 | 49 | last_changed_date = Datetime( | ||
263 | 50 | title=_('When this file was last changed.')) | ||
264 | 51 | |||
265 | 52 | def getPOFilesFor(potemplates): | ||
266 | 53 | """Return `POFiles` for each of `potemplates`, in the same order. | ||
267 | 54 | |||
268 | 55 | For any `POTemplate` that does not have a translation to the | ||
269 | 56 | required language, a `DummyPOFile` is provided. | ||
270 | 57 | """ | ||
271 | 58 | |||
272 | 59 | def setCounts(total, imported, changed, new, unreviewed, last_changed): | ||
273 | 60 | """Set aggregated message counts for ProductSeriesLanguage.""" | ||
274 | 61 | |||
275 | 62 | def recalculateCounts(total, imported, changed, new, unreviewed): | ||
276 | 63 | """Recalculate message counts for this ProductSeriesLanguage.""" | ||
277 | 64 | |||
278 | 65 | 40 | ||
279 | 66 | class IProductSeriesLanguageSet(Interface): | 41 | class IProductSeriesLanguageSet(Interface): |
280 | 67 | """The set of productserieslanguages.""" | 42 | """The set of productserieslanguages.""" |
281 | 68 | 43 | ||
282 | === added file 'lib/lp/translations/interfaces/translatedlanguage.py' | |||
283 | --- lib/lp/translations/interfaces/translatedlanguage.py 1970-01-01 00:00:00 +0000 | |||
284 | +++ lib/lp/translations/interfaces/translatedlanguage.py 2010-07-28 22:36:12 +0000 | |||
285 | @@ -0,0 +1,79 @@ | |||
286 | 1 | # Copyright 2010 Canonical Ltd. This software is licensed under the | ||
287 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
288 | 3 | |||
289 | 4 | # pylint: disable-msg=E0211,E0213 | ||
290 | 5 | |||
291 | 6 | from zope.interface import Attribute, Interface | ||
292 | 7 | from zope.interface.common.sequence import IFiniteSequence | ||
293 | 8 | from zope.schema import Datetime, Object | ||
294 | 9 | |||
295 | 10 | from canonical.launchpad import _ | ||
296 | 11 | from lp.services.worlddata.interfaces.language import ILanguage | ||
297 | 12 | from lp.translations.interfaces.potemplate import IHasTranslationTemplates | ||
298 | 13 | from lp.registry.interfaces.person import IPerson | ||
299 | 14 | |||
300 | 15 | __metaclass__ = type | ||
301 | 16 | |||
302 | 17 | __all__ = [ | ||
303 | 18 | 'IPOFilesByPOTemplates', | ||
304 | 19 | 'ITranslatedLanguage', | ||
305 | 20 | ] | ||
306 | 21 | |||
307 | 22 | |||
308 | 23 | class ITranslatedLanguage(Interface): | ||
309 | 24 | """Interface for providing translations for context by language. | ||
310 | 25 | |||
311 | 26 | It expects `parent` to provide `IHasTranslationTemplates`. | ||
312 | 27 | """ | ||
313 | 28 | |||
314 | 29 | language = Object( | ||
315 | 30 | title=_('Language to gather statistics and POFiles for.'), | ||
316 | 31 | schema=ILanguage) | ||
317 | 32 | |||
318 | 33 | parent = Object( | ||
319 | 34 | title=_('A parent with translation templates.'), | ||
320 | 35 | schema=IHasTranslationTemplates) | ||
321 | 36 | |||
322 | 37 | pofiles = Attribute( | ||
323 | 38 | _('Iterator over all POFiles for this context and language.')) | ||
324 | 39 | |||
325 | 40 | translation_statistics = Attribute( | ||
326 | 41 | _('A dict containing relevant aggregated statistics counts.')) | ||
327 | 42 | |||
328 | 43 | def setCounts(total, translated, new, changed, unreviewed): | ||
329 | 44 | """Set aggregated message counts for ITranslatedLanguage.""" | ||
330 | 45 | |||
331 | 46 | def recalculateCounts(): | ||
332 | 47 | """Recalculate message counts for this ITranslatedLanguage.""" | ||
333 | 48 | |||
334 | 49 | last_changed_date = Datetime( | ||
335 | 50 | title=_('When was this translation last changed.'), | ||
336 | 51 | readonly=False, required=True) | ||
337 | 52 | |||
338 | 53 | last_translator = Object( | ||
339 | 54 | title=_('Last person that translated something in this context.'), | ||
340 | 55 | schema=IPerson) | ||
341 | 56 | |||
342 | 57 | |||
343 | 58 | class IPOFilesByPOTemplates(IFiniteSequence): | ||
344 | 59 | """Iterate `IPOFile`s for (`ILanguage`, `ITranslationTemplateCollection`). | ||
345 | 60 | |||
346 | 61 | This is a wrapper for Storm ResultSet that enables optimized slicing | ||
347 | 62 | by doing it lazily on the query, thus allowing DummyPOFile objects | ||
348 | 63 | to be returned while still not doing more than one database query. | ||
349 | 64 | |||
350 | 65 | It subclasses `IFiniteSequence` so it can easily be used with the | ||
351 | 66 | BatchNavigator. | ||
352 | 67 | """ | ||
353 | 68 | |||
354 | 69 | def __getitem__(selector): | ||
355 | 70 | """Get an element or slice of `IPOFile`s for given templates.""" | ||
356 | 71 | |||
357 | 72 | def __getslice__(start, end): | ||
358 | 73 | """Deprecated, and implemented through __getitem__.""" | ||
359 | 74 | |||
360 | 75 | def __iter__(): | ||
361 | 76 | """Iterates over all `IPOFile`s for given templates.""" | ||
362 | 77 | |||
363 | 78 | def __len__(): | ||
364 | 79 | """Provides count of `IPOTemplate`s in a template collection.""" | ||
365 | 0 | 80 | ||
366 | === modified file 'lib/lp/translations/model/potemplate.py' | |||
367 | --- lib/lp/translations/model/potemplate.py 2010-07-23 19:44:16 +0000 | |||
368 | +++ lib/lp/translations/model/potemplate.py 2010-07-28 22:36:12 +0000 | |||
369 | @@ -1560,7 +1560,8 @@ | |||
370 | 1560 | @property | 1560 | @property |
371 | 1561 | def has_current_translation_templates(self): | 1561 | def has_current_translation_templates(self): |
372 | 1562 | """See `IHasTranslationTemplates`.""" | 1562 | """See `IHasTranslationTemplates`.""" |
374 | 1563 | return bool(self.getCurrentTranslationTemplates(just_ids=True).any()) | 1563 | return bool( |
375 | 1564 | self.getCurrentTranslationTemplates(just_ids=True).any()) | ||
376 | 1564 | 1565 | ||
377 | 1565 | def getCurrentTranslationFiles(self, just_ids=False): | 1566 | def getCurrentTranslationFiles(self, just_ids=False): |
378 | 1566 | """See `IHasTranslationTemplates`.""" | 1567 | """See `IHasTranslationTemplates`.""" |
379 | @@ -1655,10 +1656,16 @@ | |||
380 | 1655 | """ | 1656 | """ |
381 | 1656 | return self.joinInner(POFile, POTemplate.id == POFile.potemplateID) | 1657 | return self.joinInner(POFile, POTemplate.id == POFile.potemplateID) |
382 | 1657 | 1658 | ||
384 | 1658 | def joinOuterPOFile(self): | 1659 | def joinOuterPOFile(self, language=None): |
385 | 1659 | """Outer-join `POFile` into the collection. | 1660 | """Outer-join `POFile` into the collection. |
386 | 1660 | 1661 | ||
387 | 1661 | :return: A `TranslationTemplatesCollection` with an added outer | 1662 | :return: A `TranslationTemplatesCollection` with an added outer |
388 | 1662 | join to `POFile`. | 1663 | join to `POFile`. |
389 | 1663 | """ | 1664 | """ |
391 | 1664 | return self.joinOuter(POFile, POTemplate.id == POFile.potemplateID) | 1665 | if language is not None: |
392 | 1666 | return self.joinOuter( | ||
393 | 1667 | POFile, And(POTemplate.id == POFile.potemplateID, | ||
394 | 1668 | POFile.languageID == language.id)) | ||
395 | 1669 | else: | ||
396 | 1670 | return self.joinOuter( | ||
397 | 1671 | POFile, POTemplate.id == POFile.potemplateID) | ||
398 | 1665 | 1672 | ||
399 | === modified file 'lib/lp/translations/model/productserieslanguage.py' | |||
400 | --- lib/lp/translations/model/productserieslanguage.py 2010-07-19 15:38:51 +0000 | |||
401 | +++ lib/lp/translations/model/productserieslanguage.py 2010-07-28 22:36:12 +0000 | |||
402 | @@ -12,17 +12,13 @@ | |||
403 | 12 | 12 | ||
404 | 13 | from zope.interface import implements | 13 | from zope.interface import implements |
405 | 14 | 14 | ||
406 | 15 | from storm.expr import Coalesce, Sum | ||
407 | 16 | from storm.store import Store | ||
408 | 17 | |||
409 | 18 | from lp.translations.utilities.rosettastats import RosettaStats | 15 | from lp.translations.utilities.rosettastats import RosettaStats |
412 | 19 | from lp.translations.model.pofile import POFile | 16 | from lp.translations.model.translatedlanguage import TranslatedLanguageMixin |
411 | 20 | from lp.translations.model.potemplate import get_pofiles_for, POTemplate | ||
413 | 21 | from lp.translations.interfaces.productserieslanguage import ( | 17 | from lp.translations.interfaces.productserieslanguage import ( |
414 | 22 | IProductSeriesLanguage, IProductSeriesLanguageSet) | 18 | IProductSeriesLanguage, IProductSeriesLanguageSet) |
415 | 23 | 19 | ||
416 | 24 | 20 | ||
418 | 25 | class ProductSeriesLanguage(RosettaStats): | 21 | class ProductSeriesLanguage(RosettaStats, TranslatedLanguageMixin): |
419 | 26 | """See `IProductSeriesLanguage`.""" | 22 | """See `IProductSeriesLanguage`.""" |
420 | 27 | implements(IProductSeriesLanguage) | 23 | implements(IProductSeriesLanguage) |
421 | 28 | 24 | ||
422 | @@ -30,56 +26,14 @@ | |||
423 | 30 | assert 'en' != language.code, ( | 26 | assert 'en' != language.code, ( |
424 | 31 | 'English is not a translatable language.') | 27 | 'English is not a translatable language.') |
425 | 32 | RosettaStats.__init__(self) | 28 | RosettaStats.__init__(self) |
426 | 29 | TranslatedLanguageMixin.__init__(self) | ||
427 | 33 | self.productseries = productseries | 30 | self.productseries = productseries |
428 | 31 | self.parent = productseries | ||
429 | 34 | self.language = language | 32 | self.language = language |
430 | 35 | self.variant = variant | 33 | self.variant = variant |
431 | 36 | self.pofile = pofile | 34 | self.pofile = pofile |
432 | 37 | self.id = 0 | 35 | self.id = 0 |
478 | 38 | self._last_changed_date = None | 36 | self.last_changed_date = None |
434 | 39 | |||
435 | 40 | # Reset all cached counts. | ||
436 | 41 | self.setCounts() | ||
437 | 42 | |||
438 | 43 | def setCounts(self, total=0, imported=0, changed=0, new=0, | ||
439 | 44 | unreviewed=0, last_changed=None): | ||
440 | 45 | """See `IProductSeriesLanguage`.""" | ||
441 | 46 | self._messagecount = total | ||
442 | 47 | # "currentcount" in RosettaStats conflicts our recent terminology | ||
443 | 48 | # and is closer to "imported" (except that it doesn't include | ||
444 | 49 | # "changed") translations. | ||
445 | 50 | self._currentcount = imported | ||
446 | 51 | self._updatescount = changed | ||
447 | 52 | self._rosettacount = new | ||
448 | 53 | self._unreviewed_count = unreviewed | ||
449 | 54 | if last_changed is not None: | ||
450 | 55 | self._last_changed_date = last_changed | ||
451 | 56 | |||
452 | 57 | def _getMessageCount(self): | ||
453 | 58 | store = Store.of(self.language) | ||
454 | 59 | query = store.find(Sum(POTemplate.messagecount), | ||
455 | 60 | POTemplate.productseries==self.productseries, | ||
456 | 61 | POTemplate.iscurrent==True) | ||
457 | 62 | total, = query | ||
458 | 63 | if total is None: | ||
459 | 64 | total = 0 | ||
460 | 65 | return total | ||
461 | 66 | |||
462 | 67 | def recalculateCounts(self): | ||
463 | 68 | """See `IProductSeriesLanguage`.""" | ||
464 | 69 | store = Store.of(self.language) | ||
465 | 70 | query = store.find( | ||
466 | 71 | (Coalesce(Sum(POFile.currentcount), 0), | ||
467 | 72 | Coalesce(Sum(POFile.updatescount), 0), | ||
468 | 73 | Coalesce(Sum(POFile.rosettacount), 0), | ||
469 | 74 | Coalesce(Sum(POFile.unreviewed_count), 0)), | ||
470 | 75 | POFile.language==self.language, | ||
471 | 76 | POFile.variant==None, | ||
472 | 77 | POFile.potemplate==POTemplate.id, | ||
473 | 78 | POTemplate.productseries==self.productseries, | ||
474 | 79 | POTemplate.iscurrent==True) | ||
475 | 80 | imported, changed, new, unreviewed = query[0] | ||
476 | 81 | self.setCounts(self._getMessageCount(), imported, changed, | ||
477 | 82 | new, unreviewed) | ||
479 | 83 | 37 | ||
480 | 84 | @property | 38 | @property |
481 | 85 | def title(self): | 39 | def title(self): |
482 | @@ -90,46 +44,29 @@ | |||
483 | 90 | self.productseries.displayname) | 44 | self.productseries.displayname) |
484 | 91 | 45 | ||
485 | 92 | def messageCount(self): | 46 | def messageCount(self): |
488 | 93 | """See `IProductSeriesLanguage`.""" | 47 | """See `IRosettaStats`.""" |
489 | 94 | return self._messagecount | 48 | return self._translation_statistics['total_count'] |
490 | 95 | 49 | ||
491 | 96 | def currentCount(self, language=None): | 50 | def currentCount(self, language=None): |
494 | 97 | """See `IProductSeriesLanguage`.""" | 51 | """See `IRosettaStats`.""" |
495 | 98 | return self._currentcount | 52 | translated = self._translation_statistics['translated_count'] |
496 | 53 | current = translated - self.rosettaCount(language) | ||
497 | 54 | return current | ||
498 | 99 | 55 | ||
499 | 100 | def updatesCount(self, language=None): | 56 | def updatesCount(self, language=None): |
502 | 101 | """See `IProductSeriesLanguage`.""" | 57 | """See `IRosettaStats`.""" |
503 | 102 | return self._updatescount | 58 | return self._translation_statistics['changed_count'] |
504 | 103 | 59 | ||
505 | 104 | def rosettaCount(self, language=None): | 60 | def rosettaCount(self, language=None): |
508 | 105 | """See `IProductSeriesLanguage`.""" | 61 | """See `IRosettaStats`.""" |
509 | 106 | return self._rosettacount | 62 | new = self._translation_statistics['new_count'] |
510 | 63 | changed = self._translation_statistics['changed_count'] | ||
511 | 64 | rosetta = new + changed | ||
512 | 65 | return rosetta | ||
513 | 107 | 66 | ||
514 | 108 | def unreviewedCount(self): | 67 | def unreviewedCount(self): |
539 | 109 | """See `IProductSeriesLanguage`.""" | 68 | """See `IRosettaStats`.""" |
540 | 110 | return self._unreviewed_count | 69 | return self._translation_statistics['unreviewed_count'] |
517 | 111 | |||
518 | 112 | @property | ||
519 | 113 | def last_changed_date(self): | ||
520 | 114 | """See `IProductSeriesLanguage`.""" | ||
521 | 115 | return self._last_changed_date | ||
522 | 116 | |||
523 | 117 | @property | ||
524 | 118 | def pofiles(self): | ||
525 | 119 | """See `IProductSeriesLanguage`.""" | ||
526 | 120 | store = Store.of(self.language) | ||
527 | 121 | result = store.find( | ||
528 | 122 | POFile, | ||
529 | 123 | POFile.language==self.language, | ||
530 | 124 | POFile.variant==self.variant, | ||
531 | 125 | POFile.potemplate==POTemplate.id, | ||
532 | 126 | POTemplate.productseries==self.productseries, | ||
533 | 127 | POTemplate.iscurrent==True) | ||
534 | 128 | return result.order_by(['-priority']) | ||
535 | 129 | |||
536 | 130 | def getPOFilesFor(self, potemplates): | ||
537 | 131 | """See `IProductSeriesLanguage`.""" | ||
538 | 132 | return get_pofiles_for(potemplates, self.language, self.variant) | ||
541 | 133 | 70 | ||
542 | 134 | 71 | ||
543 | 135 | class ProductSeriesLanguageSet: | 72 | class ProductSeriesLanguageSet: |
544 | 136 | 73 | ||
545 | === added file 'lib/lp/translations/model/translatedlanguage.py' | |||
546 | --- lib/lp/translations/model/translatedlanguage.py 1970-01-01 00:00:00 +0000 | |||
547 | +++ lib/lp/translations/model/translatedlanguage.py 2010-07-28 22:36:12 +0000 | |||
548 | @@ -0,0 +1,132 @@ | |||
549 | 1 | # Copyright 2010 Canonical Ltd. This software is licensed under the | ||
550 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
551 | 3 | |||
552 | 4 | __all__ = ['TranslatedLanguageMixin'] | ||
553 | 5 | |||
554 | 6 | import pytz | ||
555 | 7 | |||
556 | 8 | from zope.interface import implements | ||
557 | 9 | |||
558 | 10 | from storm.expr import Coalesce, Desc, Max, Sum | ||
559 | 11 | |||
560 | 12 | from lp.translations.interfaces.potemplate import IHasTranslationTemplates | ||
561 | 13 | from lp.translations.interfaces.translatedlanguage import ( | ||
562 | 14 | IPOFilesByPOTemplates, ITranslatedLanguage) | ||
563 | 15 | from lp.translations.model.pofile import POFile | ||
564 | 16 | from lp.translations.model.potemplate import POTemplate | ||
565 | 17 | |||
566 | 18 | |||
567 | 19 | class POFilesByPOTemplates(object): | ||
568 | 20 | """See `IPOFilesByPOTemplates`.""" | ||
569 | 21 | implements(IPOFilesByPOTemplates) | ||
570 | 22 | |||
571 | 23 | def __init__(self, templates_collection, language): | ||
572 | 24 | self.templates_collection = templates_collection | ||
573 | 25 | self.language = language | ||
574 | 26 | |||
575 | 27 | def _getDummyOrPOFile(self, potemplate, pofile): | ||
576 | 28 | if pofile is None: | ||
577 | 29 | return potemplate.getDummyPOFile(self.language, | ||
578 | 30 | check_for_existing=False) | ||
579 | 31 | else: | ||
580 | 32 | return pofile | ||
581 | 33 | |||
582 | 34 | def _getPOTemplatesAndPOFilesResultSet(self): | ||
583 | 35 | current_templates = self.templates_collection | ||
584 | 36 | pofiles = current_templates.joinOuterPOFile(self.language) | ||
585 | 37 | results = pofiles.select(POTemplate, POFile).order_by( | ||
586 | 38 | Desc(POTemplate.priority), POTemplate.name) | ||
587 | 39 | return results | ||
588 | 40 | |||
589 | 41 | def _getPOFilesForResultSet(self, resultset, selector=None): | ||
590 | 42 | pofiles_list = [] | ||
591 | 43 | if selector is None: | ||
592 | 44 | results = resultset | ||
593 | 45 | else: | ||
594 | 46 | results = resultset[selector] | ||
595 | 47 | for potemplate, pofile in results: | ||
596 | 48 | pofiles_list.append(self._getDummyOrPOFile(potemplate, pofile)) | ||
597 | 49 | return pofiles_list | ||
598 | 50 | |||
599 | 51 | def __getitem__(self, selector): | ||
600 | 52 | resultset = self._getPOTemplatesAndPOFilesResultSet() | ||
601 | 53 | if isinstance(selector, slice): | ||
602 | 54 | return self._getPOFilesForResultSet(resultset, selector) | ||
603 | 55 | else: | ||
604 | 56 | potemplate, pofile = resultset[selector] | ||
605 | 57 | return self._getDummyOrPOFile(potemplate, pofile) | ||
606 | 58 | |||
607 | 59 | def __iter__(self): | ||
608 | 60 | resultset = self._getPOTemplatesAndPOFilesResultSet() | ||
609 | 61 | for pofile in self._getPOFilesForResultSet(resultset): | ||
610 | 62 | yield pofile | ||
611 | 63 | |||
612 | 64 | def __len__(self): | ||
613 | 65 | return self.templates_collection.select(POTemplate).count() | ||
614 | 66 | |||
615 | 67 | def __nonzero__(self): | ||
616 | 68 | return bool(self.templates_collection.select(POTemplate).any()) | ||
617 | 69 | |||
618 | 70 | |||
619 | 71 | class TranslatedLanguageMixin(object): | ||
620 | 72 | """See `ITranslatedLanguage`.""" | ||
621 | 73 | implements(ITranslatedLanguage) | ||
622 | 74 | |||
623 | 75 | language = None | ||
624 | 76 | parent = None | ||
625 | 77 | |||
626 | 78 | def __init__(self): | ||
627 | 79 | self.setCounts(total=0, translated=0, new=0, changed=0, unreviewed=0) | ||
628 | 80 | |||
629 | 81 | @property | ||
630 | 82 | def pofiles(self): | ||
631 | 83 | """See `ITranslatedLanguage`.""" | ||
632 | 84 | assert IHasTranslationTemplates.providedBy(self.parent), ( | ||
633 | 85 | "Parent object should implement `IHasTranslationTemplates`.") | ||
634 | 86 | current_templates = self.parent.getCurrentTemplatesCollection() | ||
635 | 87 | return POFilesByPOTemplates(current_templates, self.language) | ||
636 | 88 | |||
637 | 89 | @property | ||
638 | 90 | def translation_statistics(self): | ||
639 | 91 | """See `ITranslatedLanguage`.""" | ||
640 | 92 | # This is a temporary translation statistics 'object' to allow | ||
641 | 93 | # smoother migration from IRosettaStats to something much nicer. | ||
642 | 94 | return self._translation_statistics | ||
643 | 95 | |||
644 | 96 | def setCounts(self, total, translated, new, changed, unreviewed): | ||
645 | 97 | """See `ITranslatedLanguage`.""" | ||
646 | 98 | untranslated = total - translated | ||
647 | 99 | self._translation_statistics = { | ||
648 | 100 | 'total_count': total, | ||
649 | 101 | 'translated_count': translated, | ||
650 | 102 | 'new_count': new, | ||
651 | 103 | 'changed_count': changed, | ||
652 | 104 | 'unreviewed_count': unreviewed, | ||
653 | 105 | 'untranslated_count': untranslated, | ||
654 | 106 | } | ||
655 | 107 | |||
656 | 108 | def recalculateCounts(self): | ||
657 | 109 | """See `ITranslatedLanguage`.""" | ||
658 | 110 | templates = self.parent.getCurrentTemplatesCollection() | ||
659 | 111 | pofiles = templates.joinOuterPOFile(self.language) | ||
660 | 112 | total_count_results = list( | ||
661 | 113 | pofiles.select(Coalesce(Sum(POTemplate.messagecount), 0), | ||
662 | 114 | Coalesce(Sum(POFile.currentcount), 0), | ||
663 | 115 | Coalesce(Sum(POFile.updatescount), 0), | ||
664 | 116 | Coalesce(Sum(POFile.rosettacount), 0), | ||
665 | 117 | Coalesce(Sum(POFile.unreviewed_count), 0), | ||
666 | 118 | Max(POFile.date_changed))) | ||
667 | 119 | total, imported, changed, rosetta, unreviewed, date_changed = ( | ||
668 | 120 | total_count_results[0]) | ||
669 | 121 | translated = imported + rosetta | ||
670 | 122 | new = rosetta - changed | ||
671 | 123 | self.setCounts(total, translated, new, changed, unreviewed) | ||
672 | 124 | |||
673 | 125 | # We have to add a timezone to the otherwise naive-datetime object | ||
674 | 126 | # (because we've gotten it using Max() aggregate function). | ||
675 | 127 | if date_changed is not None: | ||
676 | 128 | date_changed = date_changed.replace(tzinfo=pytz.UTC) | ||
677 | 129 | self.last_changed_date = date_changed | ||
678 | 130 | |||
679 | 131 | last_changed_date = None | ||
680 | 132 | last_translator = None | ||
681 | 0 | 133 | ||
682 | === modified file 'lib/lp/translations/tests/test_productserieslanguage.py' | |||
683 | --- lib/lp/translations/tests/test_productserieslanguage.py 2010-07-21 09:35:41 +0000 | |||
684 | +++ lib/lp/translations/tests/test_productserieslanguage.py 2010-07-28 22:36:12 +0000 | |||
685 | @@ -89,8 +89,11 @@ | |||
686 | 89 | self.assertEquals(sr_psl.language, serbian) | 89 | self.assertEquals(sr_psl.language, serbian) |
687 | 90 | self.assertEquals(sr_psl.pofile, None) | 90 | self.assertEquals(sr_psl.pofile, None) |
688 | 91 | 91 | ||
691 | 92 | # Only this POFile is returned by the `pofiles` property. | 92 | # A POFile is returned where it exists, and a DummyPOFile where |
692 | 93 | self.assertEquals(list(sr_psl.pofiles), [pofile1]) | 93 | # it doesn't. |
693 | 94 | self.assertEquals(2, len(sr_psl.pofiles)) | ||
694 | 95 | self.assertEquals(potemplate2, sr_psl.pofiles[0].potemplate) | ||
695 | 96 | self.assertEquals(pofile1, sr_psl.pofiles[1]) | ||
696 | 94 | 97 | ||
697 | 95 | # If we provide a POFile for the other template, `pofiles` | 98 | # If we provide a POFile for the other template, `pofiles` |
698 | 96 | # returns both (ordered by decreasing priority). | 99 | # returns both (ordered by decreasing priority). |
699 | @@ -173,8 +176,9 @@ | |||
700 | 173 | self.productseries, self.language) | 176 | self.productseries, self.language) |
701 | 174 | self.assertEquals(psl.messageCount(), 0) | 177 | self.assertEquals(psl.messageCount(), 0) |
702 | 175 | 178 | ||
705 | 176 | # So, we need to get it through productseries.productserieslanguages. | 179 | # We explicitely ask for stats to be recalculated. |
706 | 177 | psl = self.productseries.productserieslanguages[0] | 180 | psl.recalculateCounts() |
707 | 181 | |||
708 | 178 | self.assertPSLStatistics(psl, | 182 | self.assertPSLStatistics(psl, |
709 | 179 | (pofile.messageCount(), | 183 | (pofile.messageCount(), |
710 | 180 | pofile.translatedCount(), | 184 | pofile.translatedCount(), |
711 | @@ -199,17 +203,14 @@ | |||
712 | 199 | self.setPOFileStatistics(pofile2, 1, 1, 1, 1, pofile2.date_changed) | 203 | self.setPOFileStatistics(pofile2, 1, 1, 1, 1, pofile2.date_changed) |
713 | 200 | 204 | ||
714 | 201 | psl = self.productseries.productserieslanguages[0] | 205 | psl = self.productseries.productserieslanguages[0] |
720 | 202 | 206 | # We explicitely ask for stats to be recalculated. | |
721 | 203 | # The psl.last_changed_date here is a naive datetime. So, for sake of | 207 | psl.recalculateCounts() |
717 | 204 | # the tests, we should make pofile2 naive when checking if it matches | ||
718 | 205 | # the last calculated changed date, that should be the same as | ||
719 | 206 | # pofile2, created last. | ||
722 | 207 | 208 | ||
723 | 208 | # Total is a sum of totals in both POTemplates (10+20). | 209 | # Total is a sum of totals in both POTemplates (10+20). |
724 | 209 | # Translated is a sum of imported and rosetta translations, | 210 | # Translated is a sum of imported and rosetta translations, |
725 | 210 | # which adds up as (4+3)+(1+1). | 211 | # which adds up as (4+3)+(1+1). |
726 | 211 | self.assertPSLStatistics(psl, (30, 9, 5, 4, 3, 6, | 212 | self.assertPSLStatistics(psl, (30, 9, 5, 4, 3, 6, |
728 | 212 | pofile2.date_changed.replace(tzinfo=None))) | 213 | pofile2.date_changed)) |
729 | 213 | self.assertPSLStatistics(psl, ( | 214 | self.assertPSLStatistics(psl, ( |
730 | 214 | pofile1.messageCount() + pofile2.messageCount(), | 215 | pofile1.messageCount() + pofile2.messageCount(), |
731 | 215 | pofile1.translatedCount() + pofile2.translatedCount(), | 216 | pofile1.translatedCount() + pofile2.translatedCount(), |
732 | @@ -217,7 +218,7 @@ | |||
733 | 217 | pofile1.rosettaCount() + pofile2.rosettaCount(), | 218 | pofile1.rosettaCount() + pofile2.rosettaCount(), |
734 | 218 | pofile1.updatesCount() + pofile2.updatesCount(), | 219 | pofile1.updatesCount() + pofile2.updatesCount(), |
735 | 219 | pofile1.unreviewedCount() + pofile2.unreviewedCount(), | 220 | pofile1.unreviewedCount() + pofile2.unreviewedCount(), |
737 | 220 | pofile2.date_changed.replace(tzinfo=None))) | 221 | pofile2.date_changed)) |
738 | 221 | 222 | ||
739 | 222 | def test_recalculateCounts(self): | 223 | def test_recalculateCounts(self): |
740 | 223 | # Test that recalculateCounts works correctly. | 224 | # Test that recalculateCounts works correctly. |
741 | @@ -236,13 +237,14 @@ | |||
742 | 236 | 237 | ||
743 | 237 | psl = self.psl_set.getProductSeriesLanguage(self.productseries, | 238 | psl = self.psl_set.getProductSeriesLanguage(self.productseries, |
744 | 238 | self.language) | 239 | self.language) |
746 | 239 | # recalculateCounts() doesn't recalculate the last changed date. | 240 | |
747 | 240 | psl.recalculateCounts() | 241 | psl.recalculateCounts() |
748 | 241 | # Total is a sum of totals in both POTemplates (10+20). | 242 | # Total is a sum of totals in both POTemplates (10+20). |
749 | 242 | # Translated is a sum of imported and rosetta translations, | 243 | # Translated is a sum of imported and rosetta translations, |
750 | 243 | # which adds up as (1+3)+(1+1). | 244 | # which adds up as (1+3)+(1+1). |
751 | 245 | # recalculateCounts() recalculates even the last changed date. | ||
752 | 244 | self.assertPSLStatistics(psl, (30, 6, 2, 4, 3, 5, | 246 | self.assertPSLStatistics(psl, (30, 6, 2, 4, 3, 5, |
754 | 245 | None)) | 247 | pofile2.date_changed)) |
755 | 246 | 248 | ||
756 | 247 | def test_recalculateCounts_no_pofiles(self): | 249 | def test_recalculateCounts_no_pofiles(self): |
757 | 248 | # Test that recalculateCounts works correctly even when there | 250 | # Test that recalculateCounts works correctly even when there |
758 | 249 | 251 | ||
759 | === added file 'lib/lp/translations/tests/test_translatedlanguage.py' | |||
760 | --- lib/lp/translations/tests/test_translatedlanguage.py 1970-01-01 00:00:00 +0000 | |||
761 | +++ lib/lp/translations/tests/test_translatedlanguage.py 2010-07-28 22:36:12 +0000 | |||
762 | @@ -0,0 +1,462 @@ | |||
763 | 1 | # Copyright 2010 Canonical Ltd. This software is licensed under the | ||
764 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
765 | 3 | |||
766 | 4 | __metaclass__ = type | ||
767 | 5 | |||
768 | 6 | from zope.component import getUtility | ||
769 | 7 | from zope.interface.verify import verifyObject | ||
770 | 8 | from zope.security.proxy import removeSecurityProxy | ||
771 | 9 | |||
772 | 10 | from lp.translations.interfaces.productserieslanguage import ( | ||
773 | 11 | IProductSeriesLanguageSet) | ||
774 | 12 | from lp.translations.interfaces.translatedlanguage import ITranslatedLanguage | ||
775 | 13 | from lp.translations.model.pofile import DummyPOFile | ||
776 | 14 | from lp.testing import TestCaseWithFactory | ||
777 | 15 | from canonical.testing import ZopelessDatabaseLayer | ||
778 | 16 | |||
779 | 17 | |||
780 | 18 | class TestTranslatedLanguageMixin(TestCaseWithFactory): | ||
781 | 19 | """Test TranslatedLanguageMixin.""" | ||
782 | 20 | |||
783 | 21 | layer = ZopelessDatabaseLayer | ||
784 | 22 | |||
785 | 23 | def setUp(self): | ||
786 | 24 | # Create a productseries that uses translations. | ||
787 | 25 | TestCaseWithFactory.setUp(self) | ||
788 | 26 | self.productseries = self.factory.makeProductSeries() | ||
789 | 27 | self.productseries.product.official_rosetta = True | ||
790 | 28 | self.parent = self.productseries | ||
791 | 29 | self.psl_set = getUtility(IProductSeriesLanguageSet) | ||
792 | 30 | self.language = self.factory.makeLanguage('sr@test') | ||
793 | 31 | |||
794 | 32 | def getTranslatedLanguage(self, language): | ||
795 | 33 | return self.psl_set.getProductSeriesLanguage(self.productseries, | ||
796 | 34 | language) | ||
797 | 35 | |||
798 | 36 | def addPOTemplate(self, number_of_potmsgsets=0, priority=0): | ||
799 | 37 | potemplate = self.factory.makePOTemplate( | ||
800 | 38 | productseries=self.productseries) | ||
801 | 39 | for sequence in range(number_of_potmsgsets): | ||
802 | 40 | self.factory.makePOTMsgSet(potemplate, sequence=sequence+1) | ||
803 | 41 | removeSecurityProxy(potemplate).messagecount = number_of_potmsgsets | ||
804 | 42 | potemplate.priority = priority | ||
805 | 43 | return potemplate | ||
806 | 44 | |||
807 | 45 | def test_interface(self): | ||
808 | 46 | translated_language = self.getTranslatedLanguage(self.language) | ||
809 | 47 | self.assertTrue(verifyObject(ITranslatedLanguage, | ||
810 | 48 | translated_language)) | ||
811 | 49 | |||
812 | 50 | def test_language(self): | ||
813 | 51 | translated_language = self.getTranslatedLanguage(self.language) | ||
814 | 52 | self.assertEqual(self.language, | ||
815 | 53 | translated_language.language) | ||
816 | 54 | |||
817 | 55 | def test_parent(self): | ||
818 | 56 | translated_language = self.getTranslatedLanguage(self.language) | ||
819 | 57 | self.assertEqual(self.parent, | ||
820 | 58 | translated_language.parent) | ||
821 | 59 | |||
822 | 60 | def test_pofiles_notemplates(self): | ||
823 | 61 | translated_language = self.getTranslatedLanguage(self.language) | ||
824 | 62 | self.assertEqual([], list(translated_language.pofiles)) | ||
825 | 63 | |||
826 | 64 | def test_pofiles_template_no_pofiles(self): | ||
827 | 65 | translated_language = self.getTranslatedLanguage(self.language) | ||
828 | 66 | potemplate = self.addPOTemplate() | ||
829 | 67 | dummy_pofile = potemplate.getDummyPOFile(self.language) | ||
830 | 68 | pofiles = list(translated_language.pofiles) | ||
831 | 69 | self.assertEqual(1, len(pofiles)) | ||
832 | 70 | |||
833 | 71 | # When there are no actual PO files, we get a DummyPOFile object | ||
834 | 72 | # instead. | ||
835 | 73 | dummy_pofile = pofiles[0] | ||
836 | 74 | naked_dummy = removeSecurityProxy(dummy_pofile) | ||
837 | 75 | self.assertEqual(DummyPOFile, type(naked_dummy)) | ||
838 | 76 | self.assertEqual(self.language, dummy_pofile.language) | ||
839 | 77 | self.assertEqual(potemplate, dummy_pofile.potemplate) | ||
840 | 78 | |||
841 | 79 | # Two queries get executed when listifying | ||
842 | 80 | # TranslatedLanguageMixin.pofiles: a len() does a count, and | ||
843 | 81 | # then all POTemplates and POFiles are fetched with the other. | ||
844 | 82 | self.assertStatementCount(2, list, translated_language.pofiles) | ||
845 | 83 | |||
846 | 84 | def test_pofiles_template_with_pofiles(self): | ||
847 | 85 | translated_language = self.getTranslatedLanguage(self.language) | ||
848 | 86 | potemplate = self.addPOTemplate() | ||
849 | 87 | pofile = self.factory.makePOFile(self.language.code, potemplate) | ||
850 | 88 | self.assertEqual([pofile], list(translated_language.pofiles)) | ||
851 | 89 | |||
852 | 90 | # Two queries get executed when listifying | ||
853 | 91 | # TranslatedLanguageMixin.pofiles: a len() does a count, and | ||
854 | 92 | # then all POTemplates and POFiles are fetched with the other. | ||
855 | 93 | self.assertStatementCount(2, list, translated_language.pofiles) | ||
856 | 94 | |||
857 | 95 | def test_pofiles_two_templates(self): | ||
858 | 96 | translated_language = self.getTranslatedLanguage(self.language) | ||
859 | 97 | # Two templates with different priorities so they get sorted | ||
860 | 98 | # appropriately. | ||
861 | 99 | potemplate1 = self.addPOTemplate(priority=2) | ||
862 | 100 | pofile1 = self.factory.makePOFile(self.language.code, potemplate1) | ||
863 | 101 | potemplate2 = self.addPOTemplate(priority=1) | ||
864 | 102 | pofile2 = self.factory.makePOFile(self.language.code, potemplate2) | ||
865 | 103 | self.assertEqual([pofile1, pofile2], | ||
866 | 104 | list(translated_language.pofiles)) | ||
867 | 105 | |||
868 | 106 | # Two queries get executed when listifying | ||
869 | 107 | # TranslatedLanguageMixin.pofiles: a len() does a count, and | ||
870 | 108 | # then all POTemplates and POFiles are fetched with the other. | ||
871 | 109 | self.assertStatementCount(2, list, translated_language.pofiles) | ||
872 | 110 | |||
873 | 111 | def test_pofiles_two_templates_one_dummy(self): | ||
874 | 112 | translated_language = self.getTranslatedLanguage(self.language) | ||
875 | 113 | # Two templates with different priorities so they get sorted | ||
876 | 114 | # appropriately. | ||
877 | 115 | potemplate1 = self.addPOTemplate(priority=2) | ||
878 | 116 | pofile1 = self.factory.makePOFile(self.language.code, potemplate1) | ||
879 | 117 | potemplate2 = self.addPOTemplate(priority=1) | ||
880 | 118 | pofiles = translated_language.pofiles | ||
881 | 119 | self.assertEqual(pofile1, pofiles[0]) | ||
882 | 120 | dummy_pofile = removeSecurityProxy(pofiles[1]) | ||
883 | 121 | self.assertEqual(DummyPOFile, type(dummy_pofile)) | ||
884 | 122 | |||
885 | 123 | # Two queries get executed when listifying | ||
886 | 124 | # TranslatedLanguageMixin.pofiles: a len() does a count, and | ||
887 | 125 | # then all POTemplates and POFiles are fetched with the other. | ||
888 | 126 | self.assertStatementCount(2, list, translated_language.pofiles) | ||
889 | 127 | |||
890 | 128 | def test_pofiles_slicing(self): | ||
891 | 129 | # Slicing still works, and always does the same constant number | ||
892 | 130 | # of queries (1). | ||
893 | 131 | translated_language = self.getTranslatedLanguage(self.language) | ||
894 | 132 | # Three templates with different priorities so they get sorted | ||
895 | 133 | # appropriately. | ||
896 | 134 | potemplate1 = self.addPOTemplate(priority=2) | ||
897 | 135 | pofile1 = self.factory.makePOFile(self.language.code, potemplate1) | ||
898 | 136 | potemplate2 = self.addPOTemplate(priority=1) | ||
899 | 137 | pofile2 = self.factory.makePOFile(self.language.code, potemplate2) | ||
900 | 138 | potemplate3 = self.addPOTemplate(priority=0) | ||
901 | 139 | |||
902 | 140 | pofiles = translated_language.pofiles[0:2] | ||
903 | 141 | self.assertEqual([pofile1, pofile2], list(pofiles)) | ||
904 | 142 | |||
905 | 143 | # Slicing executes only a single query. | ||
906 | 144 | get_slice = lambda of, start, end: list(of[start:end]) | ||
907 | 145 | self.assertStatementCount(1, get_slice, | ||
908 | 146 | translated_language.pofiles, 1, 3) | ||
909 | 147 | |||
910 | 148 | def test_pofiles_slicing_dummies(self): | ||
911 | 149 | # Slicing includes DummyPOFiles. | ||
912 | 150 | translated_language = self.getTranslatedLanguage(self.language) | ||
913 | 151 | # Three templates with different priorities so they get sorted | ||
914 | 152 | # appropriately. | ||
915 | 153 | potemplate1 = self.addPOTemplate(priority=2) | ||
916 | 154 | pofile1 = self.factory.makePOFile(self.language.code, potemplate1) | ||
917 | 155 | potemplate2 = self.addPOTemplate(priority=1) | ||
918 | 156 | pofile2 = self.factory.makePOFile(self.language.code, potemplate2) | ||
919 | 157 | potemplate3 = self.addPOTemplate(priority=0) | ||
920 | 158 | |||
921 | 159 | pofiles = translated_language.pofiles[1:3] | ||
922 | 160 | self.assertEqual(pofile2, pofiles[0]) | ||
923 | 161 | dummy_pofile = removeSecurityProxy(pofiles[1]) | ||
924 | 162 | self.assertEqual(DummyPOFile, type(dummy_pofile)) | ||
925 | 163 | |||
926 | 164 | def test_statistics_empty(self): | ||
927 | 165 | translated_language = self.getTranslatedLanguage(self.language) | ||
928 | 166 | |||
929 | 167 | expected = { | ||
930 | 168 | 'total_count': 0, | ||
931 | 169 | 'translated_count': 0, | ||
932 | 170 | 'new_count': 0, | ||
933 | 171 | 'changed_count': 0, | ||
934 | 172 | 'unreviewed_count': 0, | ||
935 | 173 | 'untranslated_count': 0, | ||
936 | 174 | } | ||
937 | 175 | self.assertEqual(expected, | ||
938 | 176 | translated_language.translation_statistics) | ||
939 | 177 | |||
940 | 178 | def test_setCounts_statistics(self): | ||
941 | 179 | translated_language = self.getTranslatedLanguage(self.language) | ||
942 | 180 | |||
943 | 181 | total = 5 | ||
944 | 182 | translated = 4 | ||
945 | 183 | new = 3 | ||
946 | 184 | changed = 2 | ||
947 | 185 | unreviewed = 1 | ||
948 | 186 | untranslated = total - translated | ||
949 | 187 | |||
950 | 188 | translated_language.setCounts( | ||
951 | 189 | total, translated, new, changed, unreviewed) | ||
952 | 190 | |||
953 | 191 | expected = { | ||
954 | 192 | 'total_count': total, | ||
955 | 193 | 'translated_count': translated, | ||
956 | 194 | 'new_count': new, | ||
957 | 195 | 'changed_count': changed, | ||
958 | 196 | 'unreviewed_count': unreviewed, | ||
959 | 197 | 'untranslated_count': untranslated, | ||
960 | 198 | } | ||
961 | 199 | self.assertEqual(expected, | ||
962 | 200 | translated_language.translation_statistics) | ||
963 | 201 | |||
964 | 202 | def test_recalculateCounts_empty(self): | ||
965 | 203 | translated_language = self.getTranslatedLanguage(self.language) | ||
966 | 204 | |||
967 | 205 | translated_language.recalculateCounts() | ||
968 | 206 | |||
969 | 207 | expected = { | ||
970 | 208 | 'total_count': 0, | ||
971 | 209 | 'translated_count': 0, | ||
972 | 210 | 'new_count': 0, | ||
973 | 211 | 'changed_count': 0, | ||
974 | 212 | 'unreviewed_count': 0, | ||
975 | 213 | 'untranslated_count': 0, | ||
976 | 214 | } | ||
977 | 215 | self.assertEqual(expected, | ||
978 | 216 | translated_language.translation_statistics) | ||
979 | 217 | |||
980 | 218 | def test_recalculateCounts_total_one_pofile(self): | ||
981 | 219 | translated_language = self.getTranslatedLanguage(self.language) | ||
982 | 220 | potemplate = self.addPOTemplate(number_of_potmsgsets=5) | ||
983 | 221 | pofile = self.factory.makePOFile(self.language.code, potemplate) | ||
984 | 222 | |||
985 | 223 | translated_language.recalculateCounts() | ||
986 | 224 | self.assertEqual( | ||
987 | 225 | 5, translated_language.translation_statistics['total_count']) | ||
988 | 226 | |||
989 | 227 | def test_recalculateCounts_total_two_pofiles(self): | ||
990 | 228 | translated_language = self.getTranslatedLanguage(self.language) | ||
991 | 229 | potemplate1 = self.addPOTemplate(number_of_potmsgsets=5) | ||
992 | 230 | pofile1 = self.factory.makePOFile(self.language.code, potemplate1) | ||
993 | 231 | potemplate2 = self.addPOTemplate(number_of_potmsgsets=3) | ||
994 | 232 | pofile2 = self.factory.makePOFile(self.language.code, potemplate2) | ||
995 | 233 | |||
996 | 234 | translated_language.recalculateCounts() | ||
997 | 235 | self.assertEqual( | ||
998 | 236 | 5+3, translated_language.translation_statistics['total_count']) | ||
999 | 237 | |||
1000 | 238 | def test_recalculateCounts_translated_one_pofile(self): | ||
1001 | 239 | translated_language = self.getTranslatedLanguage(self.language) | ||
1002 | 240 | potemplate = self.addPOTemplate(number_of_potmsgsets=5) | ||
1003 | 241 | pofile = self.factory.makePOFile(self.language.code, potemplate) | ||
1004 | 242 | naked_pofile = removeSecurityProxy(pofile) | ||
1005 | 243 | # translated count is current + rosetta | ||
1006 | 244 | naked_pofile.currentcount = 3 | ||
1007 | 245 | naked_pofile.rosettacount = 1 | ||
1008 | 246 | |||
1009 | 247 | translated_language.recalculateCounts() | ||
1010 | 248 | self.assertEqual( | ||
1011 | 249 | 4, translated_language.translation_statistics['translated_count']) | ||
1012 | 250 | |||
1013 | 251 | def test_recalculateCounts_translated_two_pofiles(self): | ||
1014 | 252 | translated_language = self.getTranslatedLanguage(self.language) | ||
1015 | 253 | potemplate1 = self.addPOTemplate(number_of_potmsgsets=5) | ||
1016 | 254 | pofile1 = self.factory.makePOFile(self.language.code, potemplate1) | ||
1017 | 255 | naked_pofile1 = removeSecurityProxy(pofile1) | ||
1018 | 256 | # translated count is current + rosetta | ||
1019 | 257 | naked_pofile1.currentcount = 3 | ||
1020 | 258 | naked_pofile1.rosettacount = 1 | ||
1021 | 259 | |||
1022 | 260 | potemplate2 = self.addPOTemplate(number_of_potmsgsets=3) | ||
1023 | 261 | pofile2 = self.factory.makePOFile(self.language.code, potemplate2) | ||
1024 | 262 | naked_pofile2 = removeSecurityProxy(pofile2) | ||
1025 | 263 | # translated count is current + rosetta | ||
1026 | 264 | naked_pofile2.currentcount = 1 | ||
1027 | 265 | naked_pofile2.rosettacount = 1 | ||
1028 | 266 | |||
1029 | 267 | translated_language.recalculateCounts() | ||
1030 | 268 | self.assertEqual( | ||
1031 | 269 | 6, translated_language.translation_statistics['translated_count']) | ||
1032 | 270 | |||
1033 | 271 | def test_recalculateCounts_changed_one_pofile(self): | ||
1034 | 272 | translated_language = self.getTranslatedLanguage(self.language) | ||
1035 | 273 | potemplate = self.addPOTemplate(number_of_potmsgsets=5) | ||
1036 | 274 | pofile = self.factory.makePOFile(self.language.code, potemplate) | ||
1037 | 275 | naked_pofile = removeSecurityProxy(pofile) | ||
1038 | 276 | # translated count is current + rosetta | ||
1039 | 277 | naked_pofile.updatescount = 3 | ||
1040 | 278 | |||
1041 | 279 | translated_language.recalculateCounts() | ||
1042 | 280 | self.assertEqual( | ||
1043 | 281 | 3, translated_language.translation_statistics['changed_count']) | ||
1044 | 282 | |||
1045 | 283 | def test_recalculateCounts_changed_two_pofiles(self): | ||
1046 | 284 | translated_language = self.getTranslatedLanguage(self.language) | ||
1047 | 285 | potemplate1 = self.addPOTemplate(number_of_potmsgsets=5) | ||
1048 | 286 | pofile1 = self.factory.makePOFile(self.language.code, potemplate1) | ||
1049 | 287 | naked_pofile1 = removeSecurityProxy(pofile1) | ||
1050 | 288 | naked_pofile1.updatescount = 3 | ||
1051 | 289 | |||
1052 | 290 | potemplate2 = self.addPOTemplate(number_of_potmsgsets=3) | ||
1053 | 291 | pofile2 = self.factory.makePOFile(self.language.code, potemplate2) | ||
1054 | 292 | naked_pofile2 = removeSecurityProxy(pofile2) | ||
1055 | 293 | naked_pofile2.updatescount = 1 | ||
1056 | 294 | |||
1057 | 295 | translated_language.recalculateCounts() | ||
1058 | 296 | self.assertEqual( | ||
1059 | 297 | 4, translated_language.translation_statistics['changed_count']) | ||
1060 | 298 | |||
1061 | 299 | def test_recalculateCounts_new_one_pofile(self): | ||
1062 | 300 | translated_language = self.getTranslatedLanguage(self.language) | ||
1063 | 301 | potemplate = self.addPOTemplate(number_of_potmsgsets=5) | ||
1064 | 302 | pofile = self.factory.makePOFile(self.language.code, potemplate) | ||
1065 | 303 | naked_pofile = removeSecurityProxy(pofile) | ||
1066 | 304 | # new count is rosetta - changed | ||
1067 | 305 | naked_pofile.rosettacount = 3 | ||
1068 | 306 | naked_pofile.updatescount = 1 | ||
1069 | 307 | |||
1070 | 308 | translated_language.recalculateCounts() | ||
1071 | 309 | self.assertEqual( | ||
1072 | 310 | 2, translated_language.translation_statistics['new_count']) | ||
1073 | 311 | |||
1074 | 312 | def test_recalculateCounts_new_two_pofiles(self): | ||
1075 | 313 | translated_language = self.getTranslatedLanguage(self.language) | ||
1076 | 314 | potemplate1 = self.addPOTemplate(number_of_potmsgsets=5) | ||
1077 | 315 | pofile1 = self.factory.makePOFile(self.language.code, potemplate1) | ||
1078 | 316 | naked_pofile1 = removeSecurityProxy(pofile1) | ||
1079 | 317 | # new count is rosetta - changed | ||
1080 | 318 | naked_pofile1.rosettacount = 3 | ||
1081 | 319 | naked_pofile1.updatescount = 1 | ||
1082 | 320 | |||
1083 | 321 | potemplate2 = self.addPOTemplate(number_of_potmsgsets=3) | ||
1084 | 322 | pofile2 = self.factory.makePOFile(self.language.code, potemplate2) | ||
1085 | 323 | naked_pofile2 = removeSecurityProxy(pofile2) | ||
1086 | 324 | # new count is rosetta - changed | ||
1087 | 325 | naked_pofile2.rosettacount = 2 | ||
1088 | 326 | naked_pofile2.updatescount = 1 | ||
1089 | 327 | |||
1090 | 328 | translated_language.recalculateCounts() | ||
1091 | 329 | self.assertEqual( | ||
1092 | 330 | 3, translated_language.translation_statistics['new_count']) | ||
1093 | 331 | |||
1094 | 332 | def test_recalculateCounts_unreviewed_one_pofile(self): | ||
1095 | 333 | translated_language = self.getTranslatedLanguage(self.language) | ||
1096 | 334 | potemplate = self.addPOTemplate(number_of_potmsgsets=5) | ||
1097 | 335 | pofile = self.factory.makePOFile(self.language.code, potemplate) | ||
1098 | 336 | naked_pofile = removeSecurityProxy(pofile) | ||
1099 | 337 | # translated count is current + rosetta | ||
1100 | 338 | naked_pofile.unreviewed_count = 3 | ||
1101 | 339 | |||
1102 | 340 | translated_language.recalculateCounts() | ||
1103 | 341 | self.assertEqual( | ||
1104 | 342 | 3, translated_language.translation_statistics['unreviewed_count']) | ||
1105 | 343 | |||
1106 | 344 | def test_recalculateCounts_unreviewed_two_pofiles(self): | ||
1107 | 345 | translated_language = self.getTranslatedLanguage(self.language) | ||
1108 | 346 | potemplate1 = self.addPOTemplate(number_of_potmsgsets=5) | ||
1109 | 347 | pofile1 = self.factory.makePOFile(self.language.code, potemplate1) | ||
1110 | 348 | naked_pofile1 = removeSecurityProxy(pofile1) | ||
1111 | 349 | naked_pofile1.unreviewed_count = 3 | ||
1112 | 350 | |||
1113 | 351 | potemplate2 = self.addPOTemplate(number_of_potmsgsets=3) | ||
1114 | 352 | pofile2 = self.factory.makePOFile(self.language.code, potemplate2) | ||
1115 | 353 | naked_pofile2 = removeSecurityProxy(pofile2) | ||
1116 | 354 | naked_pofile2.unreviewed_count = 1 | ||
1117 | 355 | |||
1118 | 356 | translated_language.recalculateCounts() | ||
1119 | 357 | self.assertEqual( | ||
1120 | 358 | 4, translated_language.translation_statistics['unreviewed_count']) | ||
1121 | 359 | |||
1122 | 360 | def test_recalculateCounts_one_pofile(self): | ||
1123 | 361 | translated_language = self.getTranslatedLanguage(self.language) | ||
1124 | 362 | potemplate = self.addPOTemplate(number_of_potmsgsets=5) | ||
1125 | 363 | pofile = self.factory.makePOFile(self.language.code, potemplate) | ||
1126 | 364 | naked_pofile = removeSecurityProxy(pofile) | ||
1127 | 365 | # translated count is current + rosetta | ||
1128 | 366 | naked_pofile.currentcount = 3 | ||
1129 | 367 | naked_pofile.rosettacount = 1 | ||
1130 | 368 | # Changed count is 'updatescount' on POFile. | ||
1131 | 369 | # It has to be lower or equal to currentcount. | ||
1132 | 370 | naked_pofile.updatescount = 1 | ||
1133 | 371 | # new is rosettacount-updatescount. | ||
1134 | 372 | naked_pofile.newcount = 0 | ||
1135 | 373 | naked_pofile.unreviewed_count = 3 | ||
1136 | 374 | |||
1137 | 375 | translated_language.recalculateCounts() | ||
1138 | 376 | |||
1139 | 377 | expected = { | ||
1140 | 378 | 'total_count': 5, | ||
1141 | 379 | 'translated_count': 4, | ||
1142 | 380 | 'new_count': 0, | ||
1143 | 381 | 'changed_count': 1, | ||
1144 | 382 | 'unreviewed_count': 3, | ||
1145 | 383 | 'untranslated_count': 1, | ||
1146 | 384 | } | ||
1147 | 385 | self.assertEqual(expected, | ||
1148 | 386 | translated_language.translation_statistics) | ||
1149 | 387 | |||
1150 | 388 | def test_recalculateCounts_two_pofiles(self): | ||
1151 | 389 | translated_language = self.getTranslatedLanguage(self.language) | ||
1152 | 390 | |||
1153 | 391 | # Set up one template with a single PO file. | ||
1154 | 392 | potemplate1 = self.addPOTemplate(number_of_potmsgsets=5) | ||
1155 | 393 | pofile1 = self.factory.makePOFile(self.language.code, potemplate1) | ||
1156 | 394 | naked_pofile1 = removeSecurityProxy(pofile1) | ||
1157 | 395 | # translated count is current + rosetta | ||
1158 | 396 | naked_pofile1.currentcount = 2 | ||
1159 | 397 | naked_pofile1.rosettacount = 2 | ||
1160 | 398 | # Changed count is 'updatescount' on POFile. | ||
1161 | 399 | # It has to be lower or equal to currentcount. | ||
1162 | 400 | # new is rosettacount-updatescount. | ||
1163 | 401 | naked_pofile1.updatescount = 1 | ||
1164 | 402 | naked_pofile1.unreviewed_count = 3 | ||
1165 | 403 | |||
1166 | 404 | # Set up second template with a single PO file. | ||
1167 | 405 | potemplate2 = self.addPOTemplate(number_of_potmsgsets=3) | ||
1168 | 406 | pofile2 = self.factory.makePOFile(self.language.code, potemplate2) | ||
1169 | 407 | naked_pofile2 = removeSecurityProxy(pofile2) | ||
1170 | 408 | # translated count is current + rosetta | ||
1171 | 409 | naked_pofile2.currentcount = 1 | ||
1172 | 410 | naked_pofile2.rosettacount = 2 | ||
1173 | 411 | # Changed count is 'updatescount' on POFile. | ||
1174 | 412 | # It has to be lower or equal to currentcount. | ||
1175 | 413 | # new is rosettacount-updatescount. | ||
1176 | 414 | naked_pofile2.updatescount = 1 | ||
1177 | 415 | naked_pofile2.unreviewed_count = 1 | ||
1178 | 416 | |||
1179 | 417 | translated_language.recalculateCounts() | ||
1180 | 418 | |||
1181 | 419 | expected = { | ||
1182 | 420 | 'total_count': 8, | ||
1183 | 421 | 'translated_count': 7, | ||
1184 | 422 | 'new_count': 2, | ||
1185 | 423 | 'changed_count': 2, | ||
1186 | 424 | 'unreviewed_count': 4, | ||
1187 | 425 | 'untranslated_count': 1, | ||
1188 | 426 | } | ||
1189 | 427 | self.assertEqual(expected, | ||
1190 | 428 | translated_language.translation_statistics) | ||
1191 | 429 | |||
1192 | 430 | def test_recalculateCounts_two_templates_one_translation(self): | ||
1193 | 431 | # Make sure recalculateCounts works even if a POFile is missing | ||
1194 | 432 | # for one of the templates. | ||
1195 | 433 | translated_language = self.getTranslatedLanguage(self.language) | ||
1196 | 434 | |||
1197 | 435 | # Set up one template with a single PO file. | ||
1198 | 436 | potemplate1 = self.addPOTemplate(number_of_potmsgsets=5) | ||
1199 | 437 | pofile1 = self.factory.makePOFile(self.language.code, potemplate1) | ||
1200 | 438 | naked_pofile1 = removeSecurityProxy(pofile1) | ||
1201 | 439 | # translated count is current + rosetta | ||
1202 | 440 | naked_pofile1.currentcount = 2 | ||
1203 | 441 | naked_pofile1.rosettacount = 2 | ||
1204 | 442 | # Changed count is 'updatescount' on POFile. | ||
1205 | 443 | # It has to be lower or equal to currentcount. | ||
1206 | 444 | # new is rosettacount-updatescount. | ||
1207 | 445 | naked_pofile1.updatescount = 1 | ||
1208 | 446 | naked_pofile1.unreviewed_count = 3 | ||
1209 | 447 | |||
1210 | 448 | # Set up second template with a single PO file. | ||
1211 | 449 | potemplate2 = self.addPOTemplate(number_of_potmsgsets=3) | ||
1212 | 450 | |||
1213 | 451 | translated_language.recalculateCounts() | ||
1214 | 452 | |||
1215 | 453 | expected = { | ||
1216 | 454 | 'total_count': 8, | ||
1217 | 455 | 'translated_count': 4, | ||
1218 | 456 | 'new_count': 1, | ||
1219 | 457 | 'changed_count': 1, | ||
1220 | 458 | 'unreviewed_count': 3, | ||
1221 | 459 | 'untranslated_count': 4, | ||
1222 | 460 | } | ||
1223 | 461 | self.assertEqual(expected, | ||
1224 | 462 | translated_language.translation_statistics) | ||
1225 | 0 | 463 | ||
1226 | === modified file 'lib/lp/translations/tests/test_translationtemplatescollection.py' | |||
1227 | --- lib/lp/translations/tests/test_translationtemplatescollection.py 2010-07-17 16:19:38 +0000 | |||
1228 | +++ lib/lp/translations/tests/test_translationtemplatescollection.py 2010-07-28 22:36:12 +0000 | |||
1229 | @@ -205,3 +205,22 @@ | |||
1230 | 205 | ] | 205 | ] |
1231 | 206 | self.assertContentEqual( | 206 | self.assertContentEqual( |
1232 | 207 | expected_outcome, joined.select(POTemplate, POFile)) | 207 | expected_outcome, joined.select(POTemplate, POFile)) |
1233 | 208 | |||
1234 | 209 | def test_joinOuterPOFile_language(self): | ||
1235 | 210 | trunk = self.factory.makeProduct().getSeries('trunk') | ||
1236 | 211 | translated_template = self.factory.makePOTemplate(productseries=trunk) | ||
1237 | 212 | untranslated_template = self.factory.makePOTemplate( | ||
1238 | 213 | productseries=trunk) | ||
1239 | 214 | nl = translated_template.newPOFile('nl') | ||
1240 | 215 | de = translated_template.newPOFile('de') | ||
1241 | 216 | |||
1242 | 217 | collection = TranslationTemplatesCollection() | ||
1243 | 218 | by_series = collection.restrictProductSeries(trunk) | ||
1244 | 219 | joined = by_series.joinOuterPOFile(language=nl.language) | ||
1245 | 220 | |||
1246 | 221 | expected_outcome = [ | ||
1247 | 222 | (translated_template, nl), | ||
1248 | 223 | (untranslated_template, None), | ||
1249 | 224 | ] | ||
1250 | 225 | self.assertContentEqual( | ||
1251 | 226 | expected_outcome, joined.select(POTemplate, POFile)) |
Hi Danilo,
The code looks OK for this branch. I like the object redesign so far. The branch is actually too large for me to review well, but I tried anyway. Here is what I saw:
• I'm scared by code that passes around large argument lists like ProductSeriesLa nguage. setCounts( ). Since PSL already recieves a reference to the pofile in it's constructor, is it possible to have PSL pull the counts from the pofile itself internally? Other options are to create a new method PSL.setCountsFr omPofile( ), or create a new object like PSLCounts(), or just pass in the calculated value: PSL.setCounts( translated) .
• interfaces/ translatedlangu age.py should have a copyright of 2010
• The test in test_TwoTemplat esWithTranslati ons() could be split up easily in a follow-up branch. That one test method has at least two, if not three, complete tests contained within it.
• I found the test names in TestTranslatedL anguageMixin a bit vague. For example, "test_parent" looks something like "test_psl_ sets_translatio n_parent" , and I can't tell from the tests alone if the test_recalculat eCounts_ * methods verify the algorithm correctly or not (maybe the Counts tests will be clearer when they are rewritten to test the new Stats object instead of the Mixin).
• It would be nice to see tests for POFilesByPOTemp lates in a follow-up branch. That way you don't have to test it indirectly with expensive and verbose translation setup code in the Mixin tests, the Mixin tests will shrink, and you may be able to skip the database entirely with some fake objects.
Otherwise, I think this looks good. With consideration of the points mentioned, r=mars.
Maris