Merge lp:~danilo/launchpad/translatedlanguage into lp:launchpad/db-devel

Proposed by Данило Шеган
Status: Rejected
Rejected by: Данило Шеган
Proposed branch: lp:~danilo/launchpad/translatedlanguage
Merge into: lp:launchpad/db-devel
Prerequisite: lp:~danilo/launchpad/pass-language-to-getdummy
Diff against target: 2258 lines (+1076/-208) (has conflicts)
39 files modified
lib/lp/code/browser/tests/test_branchlisting.py (+4/-2)
lib/lp/code/browser/tests/test_sourcepackagerecipe.py (+12/-5)
lib/lp/code/browser/tests/test_sourcepackagerecipebuild.py (+4/-1)
lib/lp/code/model/tests/test_branch.py (+4/-1)
lib/lp/code/model/tests/test_linkedbranch.py (+7/-2)
lib/lp/code/model/tests/test_sourcepackagerecipebuild.py (+5/-2)
lib/lp/registry/browser/tests/distroseries-views.txt (+1/-1)
lib/lp/registry/browser/tests/milestone-views.txt (+5/-1)
lib/lp/registry/browser/tests/productseries-views.txt (+6/-2)
lib/lp/registry/model/person.py (+1/-1)
lib/lp/registry/model/productseries.py (+4/-4)
lib/lp/registry/stories/webservice/xx-project-registry.txt (+4/-2)
lib/lp/registry/tests/test_distroseries.py (+15/-6)
lib/lp/registry/tests/test_sourcepackage.py (+9/-2)
lib/lp/soyuz/adapters/tests/test_packagelocation.py (+4/-1)
lib/lp/soyuz/browser/tests/archive-views.txt (+7/-2)
lib/lp/soyuz/browser/tests/test_distrosourcepackagerelease.py (+7/-1)
lib/lp/soyuz/browser/tests/test_sourcepackagerelease.py (+11/-5)
lib/lp/soyuz/doc/archive.txt (+9/-4)
lib/lp/soyuz/stories/soyuz/xx-distribution-archives.txt (+12/-5)
lib/lp/soyuz/tests/test_publishing.py (+9/-3)
lib/lp/testing/factory.py (+87/-11)
lib/lp/testing/tests/test_factory.py (+50/-0)
lib/lp/translations/browser/configure.zcml (+1/-1)
lib/lp/translations/browser/serieslanguage.py (+11/-8)
lib/lp/translations/browser/tests/test_breadcrumbs.py (+3/-1)
lib/lp/translations/configure.zcml (+17/-0)
lib/lp/translations/doc/translations-export-to-branch.txt (+5/-1)
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/stories/buildfarm/xx-build-summary.txt (+8/-3)
lib/lp/translations/tests/test_productserieslanguage.py (+10/-11)
lib/lp/translations/tests/test_translatedlanguage.py (+462/-0)
lib/lp/translations/tests/test_translationtemplatescollection.py (+19/-0)
utilities/make-lp-user (+4/-4)
Text conflict in lib/lp/code/interfaces/branch.py
To merge this branch: bzr merge lp:~danilo/launchpad/translatedlanguage
Reviewer Review Type Date Requested Status
Jeroen T. Vermeulen (community) Needs Resubmitting
Review via email: mp+30760@code.launchpad.net

Commit message

Provide ITranslatedLanguage interface along with a TranslatedLanguageMixin and use that for ProductSeriesLanguage implementation.

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 DistroSeriesLanguage and ProductSeriesLanguage, and to be the basis of cleaning up SourcePackageTranslations. It relies on the "parent" object implementing IHasTranslationTemplates with its getCurrentTemplatesCollection() method.

The next steps would be to switch ProductSeriesLanguage, DistroSeriesLanguage and SourcePackageTranslations to make the most of the mixin, but that would result in a huge branch (as if this one isn't big already). So, we only migrate ProductSeriesLanguage in this one (other than a few display-orienteed attributes, the only bits that remain in it are IRosettaStats methods which we want to get rid of as well).

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: POFilesByPOTemplates is an iterator-like object which allows slicing over a full set of POTemplates regardless of the presense of POFiles (when they are missing, we return DummyPOFile objects). This ensures we do a constant number of queries for every request.

Unfortunately, for listifying TranslatedLanguageMixin.pofiles when __len__ is defined on POFilesByPOTemplates (a requirement for BatchNavigator), it's always called even if using an iterator would be enough: this means 2 queries instead of 1. However, slicing always does a single query, as confirmed in the test.

It is (somewhat) indirectly unit-tested inside the TranslationTemplateMixinTest, though that's simply because 'pofiles' attribute implementation in the mixin is basically a set-up of POFilesByPOTemplates.

Full test is otherwise written in a way to make it easy to extend for testing over different types of objects implementing ITranslatedLanguage, even though it only tests ProductSeriesLanguage now.

= Tests =

 bin/test -cvvt test_translatedlanguage -t serieslanguage

= Demo & QA =

A few examples:

 https://translations.launchpad.dev/evolution/trunk/+lang/es
 https://translations.launchpad.dev/evolution/trunk/+lang/es?batch=1
 https://translations.launchpad.dev/evolution/trunk/+lang/sr (no PO files)
 https://translations.launchpad.dev/evolution/trunk/+lang/sr?batch=1

And to confirm we haven't broken DistroSeriesLanguage pages:

 https://translations.launchpad.dev/ubuntu/hoary/+lang/es
 https://translations.launchpad.dev/ubuntu/hoary/+lang/es

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/registry/model/productseries.py
  lib/lp/testing/factory.py
  lib/lp/translations/browser/configure.zcml
  lib/lp/translations/browser/serieslanguage.py
  lib/lp/translations/configure.zcml
  lib/lp/translations/interfaces/potemplate.py
  lib/lp/translations/interfaces/productserieslanguage.py
  lib/lp/translations/interfaces/translatedlanguage.py
  lib/lp/translations/model/potemplate.py
  lib/lp/translations/model/productserieslanguage.py
  lib/lp/translations/model/translatedlanguage.py
  lib/lp/translations/tests/test_productserieslanguage.py
  lib/lp/translations/tests/test_translatedlanguage.py
  lib/lp/translations/tests/test_translationtemplatescollection.py

./lib/lp/translations/interfaces/potemplate.py
     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/potemplate.py and model/potemplate.py, but linter is very buggy)

To post a comment you must log in.
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

Was this really meant to be proposed for db-devel? I'm recognizing changes from other people's branches that I reviewed this week in the diff.

review: Needs Resubmitting
Revision history for this message
Данило Шеган (danilo) wrote :

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/code/browser/tests/test_branchlisting.py'
2--- lib/lp/code/browser/tests/test_branchlisting.py 2010-05-28 09:54:45 +0000
3+++ lib/lp/code/browser/tests/test_branchlisting.py 2010-07-23 12:49:25 +0000
4@@ -32,12 +32,14 @@
5 from lp.testing import (
6 BrowserTestCase, TestCase, TestCaseWithFactory, login_person,
7 person_logged_in, time_counter)
8+from lp.testing.factory import remove_security_proxy_and_shout_at_engineer
9 from lp.testing.views import create_initialized_view
10 from canonical.launchpad.testing.pages import extract_text, find_tag_by_id
11 from canonical.launchpad.webapp import canonical_url
12 from canonical.launchpad.webapp.servers import LaunchpadTestRequest
13 from canonical.testing.layers import DatabaseFunctionalLayer
14
15+
16 class TestListingToSortOrder(TestCase):
17 """Tests for the BranchSet._listingSortToOrderBy static method.
18
19@@ -60,6 +62,7 @@
20
21 def assertSortsEqual(self, sort_one, sort_two):
22 """Assert that one list of sort specs is equal to another."""
23+
24 def sort_data(sort):
25 return sort.suffix, sort.expr
26 self.assertEqual(map(sort_data, sort_one), map(sort_data, sort_two))
27@@ -352,7 +355,7 @@
28 # series on the main site, not the code site.
29 branch = self.factory.makeProductBranch()
30 series = self.factory.makeProductSeries(product=branch.product)
31- series.branch = branch
32+ remove_security_proxy_and_shout_at_engineer(series).branch = branch
33 browser = self.getUserBrowser(
34 canonical_url(branch.product, rootsite='code'))
35 link = browser.getLink(re.compile('^' + series.name + '$'))
36@@ -402,4 +405,3 @@
37
38 def test_suite():
39 return unittest.TestLoader().loadTestsFromName(__name__)
40-
41
42=== modified file 'lib/lp/code/browser/tests/test_sourcepackagerecipe.py'
43--- lib/lp/code/browser/tests/test_sourcepackagerecipe.py 2010-07-21 21:16:54 +0000
44+++ lib/lp/code/browser/tests/test_sourcepackagerecipe.py 2010-07-23 12:49:25 +0000
45@@ -29,6 +29,7 @@
46 from lp.registry.interfaces.pocket import PackagePublishingPocket
47 from lp.soyuz.model.processor import ProcessorFamily
48 from lp.testing import ANONYMOUS, BrowserTestCase, login, logout
49+from lp.testing.factory import remove_security_proxy_and_shout_at_engineer
50
51
52 class TestCaseForRecipe(BrowserTestCase):
53@@ -45,7 +46,9 @@
54 self.squirrel = self.factory.makeDistroSeries(
55 displayname='Secret Squirrel', name='secret', version='100.04',
56 distribution=self.ppa.distribution)
57- self.squirrel.nominatedarchindep = self.squirrel.newArch(
58+ naked_squirrel = remove_security_proxy_and_shout_at_engineer(
59+ self.squirrel)
60+ naked_squirrel.nominatedarchindep = self.squirrel.newArch(
61 'i386', ProcessorFamily.get(1), False, self.chef,
62 supports_virtualized=True)
63
64@@ -494,7 +497,7 @@
65 def makeBuildJob(self, recipe):
66 """Return a build associated with a buildjob."""
67 build = self.factory.makeSourcePackageRecipeBuild(
68- recipe=recipe, distroseries=self.squirrel, archive=self.ppa )
69+ recipe=recipe, distroseries=self.squirrel, archive=self.ppa)
70 self.factory.makeSourcePackageRecipeBuildJob(recipe_build=build)
71 return build
72
73@@ -527,6 +530,7 @@
74 self.assertEqual(
75 set([build1, build2, build3, build4, build5, build6]),
76 set(view.builds))
77+
78 def set_day(build, day):
79 removeSecurityProxy(build).datebuilt = datetime(
80 2010, 03, day, tzinfo=utc)
81@@ -569,7 +573,8 @@
82 woody = self.factory.makeDistroSeries(
83 name='woody', displayname='Woody',
84 distribution=self.ppa.distribution)
85- woody.nominatedarchindep = woody.newArch(
86+ naked_woody = remove_security_proxy_and_shout_at_engineer(woody)
87+ naked_woody.nominatedarchindep = woody.newArch(
88 'i386', ProcessorFamily.get(1), False, self.factory.makePerson(),
89 supports_virtualized=True)
90
91@@ -603,7 +608,8 @@
92 woody = self.factory.makeDistroSeries(
93 name='woody', displayname='Woody',
94 distribution=self.ppa.distribution)
95- woody.nominatedarchindep = woody.newArch(
96+ naked_woody = remove_security_proxy_and_shout_at_engineer(woody)
97+ naked_woody.nominatedarchindep = woody.newArch(
98 'i386', ProcessorFamily.get(1), False, self.factory.makePerson(),
99 supports_virtualized=True)
100
101@@ -624,7 +630,8 @@
102 woody = self.factory.makeDistroSeries(
103 name='woody', displayname='Woody',
104 distribution=self.ppa.distribution)
105- woody.nominatedarchindep = woody.newArch(
106+ naked_woody = remove_security_proxy_and_shout_at_engineer(woody)
107+ naked_woody.nominatedarchindep = woody.newArch(
108 'i386', ProcessorFamily.get(1), False, self.factory.makePerson(),
109 supports_virtualized=True)
110
111
112=== modified file 'lib/lp/code/browser/tests/test_sourcepackagerecipebuild.py'
113--- lib/lp/code/browser/tests/test_sourcepackagerecipebuild.py 2010-07-19 09:22:06 +0000
114+++ lib/lp/code/browser/tests/test_sourcepackagerecipebuild.py 2010-07-23 12:49:25 +0000
115@@ -18,6 +18,7 @@
116 from lp.buildmaster.interfaces.buildbase import BuildStatus
117 from lp.soyuz.model.processor import ProcessorFamily
118 from lp.testing import ANONYMOUS, BrowserTestCase, login, logout
119+from lp.testing.factory import remove_security_proxy_and_shout_at_engineer
120
121
122 class TestSourcePackageRecipeBuild(BrowserTestCase):
123@@ -36,7 +37,9 @@
124 self.squirrel = self.factory.makeDistroSeries(
125 displayname='Secret Squirrel', name='secret', version='100.04',
126 distribution=self.ppa.distribution)
127- self.squirrel.nominatedarchindep = self.squirrel.newArch(
128+ naked_squirrel = remove_security_proxy_and_shout_at_engineer(
129+ self.squirrel)
130+ naked_squirrel.nominatedarchindep = self.squirrel.newArch(
131 'i386', ProcessorFamily.get(1), False, self.chef,
132 supports_virtualized=True)
133
134
135=== modified file 'lib/lp/code/model/tests/test_branch.py'
136--- lib/lp/code/model/tests/test_branch.py 2010-07-17 23:15:30 +0000
137+++ lib/lp/code/model/tests/test_branch.py 2010-07-23 12:49:25 +0000
138@@ -537,7 +537,7 @@
139 jobs = list(getUtility(IBranchUpgradeJobSource).iterReady())
140 self.assertEqual(
141 jobs,
142- [job,])
143+ [job, ])
144
145 def test_requestUpgrade_no_upgrade_needed(self):
146 # If a branch doesn't need to be upgraded, requestUpgrade raises an
147@@ -635,6 +635,7 @@
148 branch = self.factory.makeProductBranch(
149 product=fooix, owner=eric, name='trunk')
150 linked_branch = ICanHasLinkedBranch(future)
151+ login_person(fooix.owner)
152 linked_branch.setBranch(branch)
153 self.assertEqual(
154 [linked_branch],
155@@ -827,6 +828,7 @@
156 product = branch.product
157 series = self.factory.makeProductSeries(product=product)
158 linked_branch = ICanHasLinkedBranch(series)
159+ login_person(series.owner)
160 linked_branch.setBranch(branch)
161 self.assertBzrIdentity(branch, linked_branch.bzr_path)
162
163@@ -852,6 +854,7 @@
164 removeSecurityProxy(branch.product))
165 series_link = ICanHasLinkedBranch(series)
166 product_link.setBranch(branch)
167+ login_person(series.owner)
168 series_link.setBranch(branch)
169 self.assertBzrIdentity(branch, product_link.bzr_path)
170
171
172=== modified file 'lib/lp/code/model/tests/test_linkedbranch.py'
173--- lib/lp/code/model/tests/test_linkedbranch.py 2010-04-16 15:06:55 +0000
174+++ lib/lp/code/model/tests/test_linkedbranch.py 2010-07-23 12:49:25 +0000
175@@ -18,6 +18,7 @@
176 from lp.registry.interfaces.distroseries import NoSuchDistroSeries
177 from lp.registry.interfaces.pocket import PackagePublishingPocket
178 from lp.testing import run_with_login, TestCaseWithFactory
179+from lp.testing.factory import remove_security_proxy_and_shout_at_engineer
180
181
182 class TestProductSeriesLinkedBranch(TestCaseWithFactory):
183@@ -27,7 +28,9 @@
184 def test_branch(self):
185 # The linked branch of a product series is its branch attribute.
186 product_series = self.factory.makeProductSeries()
187- product_series.branch = self.factory.makeProductBranch(
188+ naked_product_series = remove_security_proxy_and_shout_at_engineer(
189+ product_series)
190+ naked_product_series.branch = self.factory.makeProductBranch(
191 product=product_series.product)
192 self.assertEqual(
193 product_series.branch, ICanHasLinkedBranch(product_series).branch)
194@@ -35,9 +38,11 @@
195 def test_setBranch(self):
196 # setBranch sets the linked branch of the product series.
197 product_series = self.factory.makeProductSeries()
198+ naked_product_series = remove_security_proxy_and_shout_at_engineer(
199+ product_series)
200 branch = self.factory.makeProductBranch(
201 product=product_series.product)
202- ICanHasLinkedBranch(product_series).setBranch(branch)
203+ ICanHasLinkedBranch(naked_product_series).setBranch(branch)
204 self.assertEqual(branch, product_series.branch)
205
206 def test_bzr_path(self):
207
208=== modified file 'lib/lp/code/model/tests/test_sourcepackagerecipebuild.py'
209--- lib/lp/code/model/tests/test_sourcepackagerecipebuild.py 2010-07-20 12:12:46 +0000
210+++ lib/lp/code/model/tests/test_sourcepackagerecipebuild.py 2010-07-23 12:49:25 +0000
211@@ -37,6 +37,7 @@
212 from lp.soyuz.model.processor import ProcessorFamily
213 from lp.soyuz.tests.soyuzbuilddhelpers import WaitingSlave
214 from lp.testing import ANONYMOUS, login, person_logged_in, TestCaseWithFactory
215+from lp.testing.factory import remove_security_proxy_and_shout_at_engineer
216 from lp.testing.fakemethod import FakeMethod
217 from lp.testing.mail_helpers import pop_notifications
218
219@@ -53,7 +54,9 @@
220 distroseries_i386 = distroseries.newArch(
221 'i386', ProcessorFamily.get(1), False, person,
222 supports_virtualized=True)
223- distroseries.nominatedarchindep = distroseries_i386
224+ naked_distroseries = remove_security_proxy_and_shout_at_engineer(
225+ distroseries)
226+ naked_distroseries.nominatedarchindep = distroseries_i386
227
228 return getUtility(ISourcePackageRecipeBuildSource).new(
229 distroseries=distroseries,
230@@ -320,7 +323,7 @@
231 removeSecurityProxy(build).buildstate = BuildStatus.FULLYBUILT
232 IStore(build).flush()
233 build.notify()
234- (message,) = pop_notifications()
235+ (message, ) = pop_notifications()
236 requester = build.requester
237 requester_address = format_address(
238 requester.displayname, requester.preferredemail.email)
239
240=== modified file 'lib/lp/registry/browser/tests/distroseries-views.txt'
241--- lib/lp/registry/browser/tests/distroseries-views.txt 2010-05-24 22:04:19 +0000
242+++ lib/lp/registry/browser/tests/distroseries-views.txt 2010-07-23 12:49:25 +0000
243@@ -196,7 +196,7 @@
244 >>> [field.__name__ for field in view.form_fields]
245 ['displayname', 'title', 'summary', 'description', 'status']
246
247- >>> print view.widgets.get('status')._getFormValue()
248+ >>> print view.widgets.get('status')._getFormValue().title
249 Active Development
250
251
252
253=== modified file 'lib/lp/registry/browser/tests/milestone-views.txt'
254--- lib/lp/registry/browser/tests/milestone-views.txt 2010-06-30 19:37:09 +0000
255+++ lib/lp/registry/browser/tests/milestone-views.txt 2010-07-23 12:49:25 +0000
256@@ -579,9 +579,13 @@
257 The driver of a series that belongs to an `IDerivativeDistribution` is a
258 release manager and can create milestones.
259
260+ >>> from lp.testing.factory import (
261+ ... remove_security_proxy_and_shout_at_engineer)
262 >>> distroseries = factory.makeDistroRelease(name='pumpkin')
263 >>> driver = factory.makePerson(name='a-driver')
264- >>> distroseries.driver = driver
265+ >>> naked_distroseries = remove_security_proxy_and_shout_at_engineer(
266+ ... distroseries)
267+ >>> naked_distroseries.driver = driver
268 >>> login_person(driver)
269
270 >>> form = {
271
272=== modified file 'lib/lp/registry/browser/tests/productseries-views.txt'
273--- lib/lp/registry/browser/tests/productseries-views.txt 2010-06-13 02:01:25 +0000
274+++ lib/lp/registry/browser/tests/productseries-views.txt 2010-07-23 12:49:25 +0000
275@@ -255,6 +255,8 @@
276
277 >>> from datetime import datetime
278 >>> from pytz import UTC
279+ >>> from lp.testing.factory import (
280+ ... remove_security_proxy_and_shout_at_engineer)
281
282 >>> product = factory.makeProduct(name="field", displayname='Field')
283 >>> productseries = factory.makeProductSeries(
284@@ -263,7 +265,9 @@
285
286 # Hack the creation date for testing purposes.
287 >>> test_date = datetime(2009, 05, 01, 19, 34, 24, tzinfo=UTC)
288- >>> productseries.datecreated = test_date
289+ >>> naked_productseries = remove_security_proxy_and_shout_at_engineer(
290+ ... productseries)
291+ >>> naked_productseries.datecreated = test_date
292
293 Users without edit permission cannot access the view.
294
295@@ -470,7 +474,7 @@
296
297 The series status is set to obsolete and the releasefileglob was set to None.
298
299- >>> print productseries.status
300+ >>> print productseries.status.title
301 Obsolete
302 >>> print productseries.releasefileglob
303 None
304
305=== modified file 'lib/lp/registry/model/person.py'
306--- lib/lp/registry/model/person.py 2010-07-22 05:11:59 +0000
307+++ lib/lp/registry/model/person.py 2010-07-23 12:49:25 +0000
308@@ -2495,7 +2495,7 @@
309 creation_rationale, comment=comment)
310 db_updated = True
311
312- return IPerson(account), db_updated
313+ return IPerson(account), db_updated
314
315 def newTeam(self, teamowner, name, displayname, teamdescription=None,
316 subscriptionpolicy=TeamSubscriptionPolicy.MODERATED,
317
318=== modified file 'lib/lp/registry/model/productseries.py'
319--- lib/lp/registry/model/productseries.py 2010-07-15 15:01:18 +0000
320+++ lib/lp/registry/model/productseries.py 2010-07-23 12:49:25 +0000
321@@ -469,8 +469,8 @@
322 pofile.currentCount(),
323 pofile.updatesCount(),
324 pofile.rosettaCount(),
325- pofile.unreviewedCount(),
326- pofile.date_changed)
327+ pofile.unreviewedCount())
328+ psl.last_changed_date = pofile.date_changed
329 results.append(psl)
330 else:
331 # If there is more than one template, do a single
332@@ -512,8 +512,8 @@
333 for (language, imported, changed, new, unreviewed,
334 last_changed) in ordered_results:
335 psl = ProductSeriesLanguage(self, language)
336- psl.setCounts(
337- total, imported, changed, new, unreviewed, last_changed)
338+ psl.setCounts(total, imported, changed, new, unreviewed)
339+ psl.last_changed_date = last_changed
340 results.append(psl)
341
342 return results
343
344=== modified file 'lib/lp/registry/stories/webservice/xx-project-registry.txt'
345--- lib/lp/registry/stories/webservice/xx-project-registry.txt 2010-06-14 18:32:58 +0000
346+++ lib/lp/registry/stories/webservice/xx-project-registry.txt 2010-07-23 12:49:25 +0000
347@@ -847,15 +847,17 @@
348 The entry for a project series is available at its canonical URL on the
349 virtual host.
350
351+ >>> from zope.security.proxy import removeSecurityProxy
352 >>> login('test@canonical.com')
353 >>> babadoo_owner = factory.makePerson(name='babadoo-owner')
354 >>> babadoo = factory.makeProduct(name='babadoo', owner=babadoo_owner)
355 >>> foobadoo = factory.makeProductSeries(
356 ... product=babadoo, name='foobadoo', owner=babadoo_owner)
357- >>> foobadoo.summary = u'Foobadoo support for Babadoo'
358+ >>> removeSecurityProxy(foobadoo).summary = (
359+ ... u'Foobadoo support for Babadoo')
360 >>> fooey = factory.makeAnyBranch(
361 ... product=babadoo, name='fooey', owner=babadoo_owner)
362- >>> foobadoo.branch = fooey
363+ >>> removeSecurityProxy(foobadoo).branch = fooey
364 >>> logout()
365
366 >>> babadoo_foobadoo = webservice.get('/babadoo/foobadoo').jsonBody()
367
368=== modified file 'lib/lp/registry/tests/test_distroseries.py'
369--- lib/lp/registry/tests/test_distroseries.py 2010-06-23 23:23:28 +0000
370+++ lib/lp/registry/tests/test_distroseries.py 2010-07-23 12:49:25 +0000
371@@ -24,6 +24,7 @@
372 active_publishing_status, PackagePublishingStatus)
373 from lp.soyuz.model.processor import ProcessorFamilySet
374 from lp.testing import TestCase, TestCaseWithFactory
375+from lp.testing.factory import remove_security_proxy_and_shout_at_engineer
376 from lp.soyuz.tests.test_publishing import SoyuzTestPublisher
377 from lp.translations.interfaces.translations import (
378 TranslationsBranchImportMode)
379@@ -285,7 +286,9 @@
380 self.linkPackage('hot')
381 self.makeSeriesPackage('cold')
382 product_series = self.linkPackage('cold')
383- product_series.product.bugtraker = self.factory.makeBugTracker()
384+ naked_product_series = remove_security_proxy_and_shout_at_engineer(
385+ product_series)
386+ naked_product_series.product.bugtraker = self.factory.makeBugTracker()
387 packagings = self.series.getPrioritizedlPackagings()
388 names = [packaging.sourcepackagename.name for packaging in packagings]
389 expected = [u'hot', u'cold', u'linked']
390@@ -296,7 +299,9 @@
391 self.linkPackage('translatable')
392 self.makeSeriesPackage('withbranch')
393 product_series = self.linkPackage('withbranch')
394- product_series.branch = self.factory.makeBranch()
395+ naked_product_series = remove_security_proxy_and_shout_at_engineer(
396+ product_series)
397+ naked_product_series.branch = self.factory.makeBranch()
398 packagings = self.series.getPrioritizedlPackagings()
399 names = [packaging.sourcepackagename.name for packaging in packagings]
400 expected = [u'translatable', u'linked', u'withbranch']
401@@ -308,8 +313,10 @@
402 self.linkPackage('translatable')
403 self.makeSeriesPackage('importabletranslatable')
404 product_series = self.linkPackage('importabletranslatable')
405- product_series.branch = self.factory.makeBranch()
406- product_series.translations_autoimport_mode = (
407+ naked_product_series = remove_security_proxy_and_shout_at_engineer(
408+ product_series)
409+ naked_product_series.branch = self.factory.makeBranch()
410+ naked_product_series.translations_autoimport_mode = (
411 TranslationsBranchImportMode.IMPORT_TEMPLATES)
412 packagings = self.series.getPrioritizedlPackagings()
413 names = [packaging.sourcepackagename.name for packaging in packagings]
414@@ -343,7 +350,9 @@
415
416 new_distroseries = (
417 self.factory.makeDistroRelease(name=u"sampleseries"))
418- new_distroseries.hide_all_translations = False
419+ naked_new_distroseries = remove_security_proxy_and_shout_at_engineer(
420+ new_distroseries)
421+ naked_new_distroseries.hide_all_translations = False
422 transaction.commit()
423 translatables = self._get_translatables()
424 self.failUnlessEqual(
425@@ -365,7 +374,7 @@
426 translatables,
427 self._ref_translatables(u"sampleseries")))
428
429- new_distroseries.hide_all_translations = True
430+ naked_new_distroseries.hide_all_translations = True
431 transaction.commit()
432 translatables = self._get_translatables()
433 self.failUnlessEqual(
434
435=== modified file 'lib/lp/registry/tests/test_sourcepackage.py'
436--- lib/lp/registry/tests/test_sourcepackage.py 2010-02-24 03:06:54 +0000
437+++ lib/lp/registry/tests/test_sourcepackage.py 2010-07-23 12:49:25 +0000
438@@ -22,6 +22,7 @@
439 from lp.code.interfaces.seriessourcepackagebranch import (
440 IMakeOfficialBranchLinks)
441 from lp.testing import TestCaseWithFactory
442+from lp.testing.factory import remove_security_proxy_and_shout_at_engineer
443 from lp.testing.views import create_initialized_view
444 from canonical.testing.layers import DatabaseFunctionalLayer
445
446@@ -252,11 +253,17 @@
447
448 self.obsolete_productseries = self.factory.makeProductSeries(
449 name='obsolete', product=self.product)
450- self.obsolete_productseries.status = SeriesStatus.OBSOLETE
451+ naked_obsolete_productseries = (
452+ remove_security_proxy_and_shout_at_engineer(
453+ self.obsolete_productseries))
454+ naked_obsolete_productseries.status = SeriesStatus.OBSOLETE
455
456 self.dev_productseries = self.factory.makeProductSeries(
457 name='current', product=self.product)
458- self.dev_productseries.status = SeriesStatus.DEVELOPMENT
459+ naked_dev_productseries = (
460+ remove_security_proxy_and_shout_at_engineer(
461+ self.dev_productseries))
462+ naked_dev_productseries.status = SeriesStatus.DEVELOPMENT
463
464 self.distribution = self.factory.makeDistribution(
465 name='youbuntu', displayname='Youbuntu', owner=self.owner)
466
467=== modified file 'lib/lp/soyuz/adapters/tests/test_packagelocation.py'
468--- lib/lp/soyuz/adapters/tests/test_packagelocation.py 2010-06-22 12:08:51 +0000
469+++ lib/lp/soyuz/adapters/tests/test_packagelocation.py 2010-07-23 12:49:25 +0000
470@@ -12,8 +12,10 @@
471 from lp.soyuz.interfaces.archive import ArchivePurpose
472 from lp.soyuz.interfaces.component import IComponentSet
473 from lp.testing import TestCaseWithFactory
474+from lp.testing.factory import remove_security_proxy_and_shout_at_engineer
475 from canonical.testing import LaunchpadZopelessLayer
476
477+
478 class TestPackageLocation(TestCaseWithFactory):
479 """Test the `PackageLocation` class."""
480 layer = LaunchpadZopelessLayer
481@@ -34,7 +36,8 @@
482 returned_location = self.factory.makeCopyArchiveLocation(
483 distribution=ubuntu, name='now-comes-the-mystery',
484 owner=self.factory.makePerson(name='mysteryman'))
485- copy_archive = returned_location.archive
486+ copy_archive = remove_security_proxy_and_shout_at_engineer(
487+ returned_location).archive
488
489 # Now use the created copy archive to test the build_package_location
490 # helper (called via getPackageLocation):
491
492=== modified file 'lib/lp/soyuz/browser/tests/archive-views.txt'
493--- lib/lp/soyuz/browser/tests/archive-views.txt 2010-05-21 10:59:09 +0000
494+++ lib/lp/soyuz/browser/tests/archive-views.txt 2010-07-23 12:49:25 +0000
495@@ -15,11 +15,14 @@
496
497 >>> from lp.registry.interfaces.distribution import (
498 ... IDistributionSet)
499+ >>> from lp.testing.factory import (
500+ ... remove_security_proxy_and_shout_at_engineer)
501 >>> ubuntu = getUtility(IDistributionSet)['ubuntu']
502 >>> copy_location = factory.makeCopyArchiveLocation(
503 ... distribution=ubuntu,
504 ... name="intrepid-security-rebuild")
505- >>> copy_archive = copy_location.archive
506+ >>> copy_archive = remove_security_proxy_and_shout_at_engineer(
507+ ... copy_location).archive
508
509 And let's create two views to compare:
510
511@@ -65,8 +68,10 @@
512 0
513
514 # Create a copy-request to Celso's PPA.
515+ >>> naked_copy_location = remove_security_proxy_and_shout_at_engineer(
516+ ... copy_location)
517 >>> package_copy_request = ubuntu.main_archive.requestPackageCopy(
518- ... copy_location, copy_archive.owner)
519+ ... naked_copy_location, copy_archive.owner)
520
521 >>> len(copy_archive_view.package_copy_requests)
522 1
523
524=== modified file 'lib/lp/soyuz/browser/tests/test_distrosourcepackagerelease.py'
525--- lib/lp/soyuz/browser/tests/test_distrosourcepackagerelease.py 2010-07-20 12:06:36 +0000
526+++ lib/lp/soyuz/browser/tests/test_distrosourcepackagerelease.py 2010-07-23 12:49:25 +0000
527@@ -14,6 +14,7 @@
528 DistributionSourcePackageRelease)
529 from lp.soyuz.tests.test_publishing import SoyuzTestPublisher
530 from lp.testing import TestCaseWithFactory
531+from lp.testing.factory import remove_security_proxy_and_shout_at_engineer
532 from lp.testing.views import create_initialized_view
533
534
535@@ -27,7 +28,12 @@
536 # The package must be published for the page to render.
537 stp = SoyuzTestPublisher()
538 distroseries = stp.setUpDefaultDistroSeries()
539- distro = distroseries.distribution
540+ naked_distroseries = remove_security_proxy_and_shout_at_engineer(
541+ distroseries)
542+ # XXX Abel Deuring, 2010-07-21, bug 608240. This is scary. But
543+ # if we use distroseries.distribution instead,
544+ # test_spr_files_deleted() and test_spr_files_one() fail.
545+ distro = naked_distroseries.distribution
546 source_package_release = stp.getPubSource().sourcepackagerelease
547 self.dspr = DistributionSourcePackageRelease(
548 distro, source_package_release)
549
550=== modified file 'lib/lp/soyuz/browser/tests/test_sourcepackagerelease.py'
551--- lib/lp/soyuz/browser/tests/test_sourcepackagerelease.py 2010-07-20 12:06:36 +0000
552+++ lib/lp/soyuz/browser/tests/test_sourcepackagerelease.py 2010-07-23 12:49:25 +0000
553@@ -16,6 +16,7 @@
554 from canonical.testing import (
555 DatabaseFunctionalLayer, LaunchpadFunctionalLayer)
556 from lp.testing import TestCaseWithFactory
557+from lp.testing.factory import remove_security_proxy_and_shout_at_engineer
558 from lp.testing.views import create_initialized_view
559
560
561@@ -64,20 +65,23 @@
562
563 def test_highlighted_copyright_is_None(self):
564 expected = ''
565- self.source_package_release.copyright = None
566+ remove_security_proxy_and_shout_at_engineer(
567+ self.source_package_release).copyright = None
568 view = create_initialized_view(
569 self.source_package_release, '+copyright')
570 self.assertEqual(expected, view.highlighted_copyright)
571
572 def test_highlighted_copyright_no_matches(self):
573 expected = 'nothing to see and/or do.'
574- self.source_package_release.copyright = expected
575+ remove_security_proxy_and_shout_at_engineer(
576+ self.source_package_release).copyright = expected
577 view = create_initialized_view(
578 self.source_package_release, '+copyright')
579 self.assertEqual(expected, view.highlighted_copyright)
580
581 def test_highlighted_copyright_match_url(self):
582- self.source_package_release.copyright = (
583+ remove_security_proxy_and_shout_at_engineer(
584+ self.source_package_release).copyright = (
585 'Downloaded from https://upstream.dom/fnord/no/ and')
586 expected = (
587 'Downloaded from '
588@@ -88,7 +92,8 @@
589 self.assertEqual(expected, view.highlighted_copyright)
590
591 def test_highlighted_copyright_match_path(self):
592- self.source_package_release.copyright = (
593+ remove_security_proxy_and_shout_at_engineer(
594+ self.source_package_release).copyright = (
595 'See /usr/share/common-licenses/GPL')
596 expected = (
597 'See '
598@@ -98,7 +103,8 @@
599 self.assertEqual(expected, view.highlighted_copyright)
600
601 def test_highlighted_copyright_match_multiple(self):
602- self.source_package_release.copyright = (
603+ remove_security_proxy_and_shout_at_engineer(
604+ self.source_package_release).copyright = (
605 'See /usr/share/common-licenses/GPL or https://osi.org/mit')
606 expected = (
607 'See '
608
609=== modified file 'lib/lp/soyuz/doc/archive.txt'
610--- lib/lp/soyuz/doc/archive.txt 2010-07-21 09:41:48 +0000
611+++ lib/lp/soyuz/doc/archive.txt 2010-07-23 12:49:25 +0000
612@@ -1285,10 +1285,15 @@
613 The IArchive interface includes a convenience method for creating a
614 package copy request:
615
616+ >>> from lp.testing.factory import (
617+ ... remove_security_proxy_and_shout_at_engineer)
618 >>> requestor = factory.makePerson(name='me-copy')
619 >>> copy_target = factory.makeCopyArchiveLocation(
620 ... distribution=ubuntu, name='my-copy-archive', owner=requestor)
621- >>> pcr = ubuntu.main_archive.requestPackageCopy(copy_target, requestor)
622+ >>> naked_copy_target = remove_security_proxy_and_shout_at_engineer(
623+ ... copy_target)
624+ >>> pcr = ubuntu.main_archive.requestPackageCopy(
625+ ... naked_copy_target, requestor)
626 >>> print pcr
627 Package copy request
628 source = primary/hoary/-/RELEASE
629@@ -1301,7 +1306,7 @@
630 The requestPackageCopy method can also take an optional suite name:
631
632 >>> package_copy_request = ubuntu.main_archive.requestPackageCopy(
633- ... copy_target, requestor, suite="hoary-updates");
634+ ... naked_copy_target, requestor, suite="hoary-updates");
635 >>> print package_copy_request
636 Package copy request
637 source = primary/hoary/-/UPDATES
638@@ -1412,9 +1417,9 @@
639
640 COPY archives use a URL format of <distro-name>-<archive-name>:
641
642- >>> print copy_target.archive.is_copy
643+ >>> print naked_copy_target.archive.is_copy
644 True
645- >>> print copy_target.archive.archive_url
646+ >>> print naked_copy_target.archive.archive_url
647 http://rebuild-test.internal/ubuntu-my-copy-archive/ubuntu
648
649 If the archive is private, the url may be different as private PPAs
650
651=== modified file 'lib/lp/soyuz/stories/soyuz/xx-distribution-archives.txt'
652--- lib/lp/soyuz/stories/soyuz/xx-distribution-archives.txt 2010-01-12 08:24:36 +0000
653+++ lib/lp/soyuz/stories/soyuz/xx-distribution-archives.txt 2010-07-23 12:49:25 +0000
654@@ -6,6 +6,8 @@
655 >>> from canonical.launchpad.interfaces import (
656 ... IDistributionSet)
657 >>> from lp.registry.interfaces.person import IPersonSet
658+ >>> from lp.testing.factory import (
659+ ... remove_security_proxy_and_shout_at_engineer)
660 >>> login('foo.bar@canonical.com')
661 >>> ubuntu = getUtility(IDistributionSet)['ubuntu']
662
663@@ -13,18 +15,21 @@
664 >>> copy_location = factory.makeCopyArchiveLocation(
665 ... distribution=ubuntu, owner=joe,
666 ... name="intrepid-security-rebuild")
667- >>> copy_archive = copy_location.archive
668+ >>> naked_copy_location = remove_security_proxy_and_shout_at_engineer(
669+ ... copy_location)
670+ >>> copy_archive = naked_copy_location.archive
671 >>> copy_archive.enabled
672 True
673
674 >>> package_copy_request = ubuntu.main_archive.requestPackageCopy(
675- ... copy_location, copy_archive.owner)
676+ ... naked_copy_location, copy_archive.owner)
677
678 >>> nopriv = getUtility(IPersonSet).getByName('no-priv')
679 >>> disabled_location = factory.makeCopyArchiveLocation(
680 ... distribution=ubuntu, owner=nopriv,
681 ... name="disabled-security-rebuild", enabled=False)
682- >>> disabled_archive = disabled_location.archive
683+ >>> disabled_archive = remove_security_proxy_and_shout_at_engineer(
684+ ... disabled_location).archive
685 >>> disabled_archive.enabled
686 False
687
688@@ -119,12 +124,14 @@
689 >>> copy_location = factory.makeCopyArchiveLocation(
690 ... distribution=ubuntu,
691 ... name="intrepid-private-security-rebuild")
692- >>> copy_archive = copy_location.archive
693+ >>> naked_copy_location = remove_security_proxy_and_shout_at_engineer(
694+ ... copy_location)
695+ >>> copy_archive = naked_copy_location.archive
696 >>> copy_archive.buildd_secret = 'really secret'
697 >>> copy_archive.private = True
698 >>> copy_archive.owner.displayname = "Harry Potter"
699 >>> package_copy_request = ubuntu.main_archive.requestPackageCopy(
700- ... copy_location, copy_archive.owner)
701+ ... naked_copy_location, copy_archive.owner)
702 >>> pub_src = stp.getPubSource(
703 ... archive=copy_archive, architecturehintlist='any')
704 >>> pub_bins = stp.getPubBinaries(pub_source=pub_src)
705
706=== modified file 'lib/lp/soyuz/tests/test_publishing.py'
707--- lib/lp/soyuz/tests/test_publishing.py 2010-07-23 12:49:10 +0000
708+++ lib/lp/soyuz/tests/test_publishing.py 2010-07-23 12:49:25 +0000
709@@ -43,7 +43,8 @@
710 from lp.soyuz.interfaces.queue import PackageUploadStatus
711 from canonical.launchpad.scripts import FakeLogger
712 from lp.testing import TestCaseWithFactory
713-from lp.testing.factory import LaunchpadObjectFactory
714+from lp.testing.factory import (
715+ LaunchpadObjectFactory, remove_security_proxy_and_shout_at_engineer)
716 from lp.testing.fakemethod import FakeMethod
717
718
719@@ -145,7 +146,10 @@
720 PackageUploadStatus.DONE: 'setDone',
721 PackageUploadStatus.ACCEPTED: 'setAccepted',
722 }
723- method = getattr(package_upload, status_to_method[upload_status])
724+ naked_package_upload = remove_security_proxy_and_shout_at_engineer(
725+ package_upload)
726+ method = getattr(
727+ naked_package_upload, status_to_method[upload_status])
728 method()
729
730 return package_upload
731@@ -221,7 +225,9 @@
732 changes_file_name=changes_file_name,
733 changes_file_content=changes_file_content,
734 upload_status=upload_status)
735- package_upload.addSource(spr)
736+ naked_package_upload = remove_security_proxy_and_shout_at_engineer(
737+ package_upload)
738+ naked_package_upload.addSource(spr)
739
740 if filename is None:
741 filename = "%s_%s.dsc" % (sourcename, version)
742
743=== modified file 'lib/lp/testing/factory.py'
744--- lib/lp/testing/factory.py 2010-07-23 12:49:10 +0000
745+++ lib/lp/testing/factory.py 2010-07-23 12:49:25 +0000
746@@ -13,8 +13,10 @@
747 __metaclass__ = type
748 __all__ = [
749 'GPGSigningContext',
750+ 'is_security_proxied_or_harmless',
751 'LaunchpadObjectFactory',
752 'ObjectFactory',
753+ 'remove_security_proxy_and_shout_at_engineer',
754 ]
755
756 from contextlib import nested
757@@ -25,18 +27,22 @@
758 from email.mime.text import MIMEText
759 from email.mime.multipart import MIMEMultipart
760 from itertools import count
761+from operator import isSequenceType
762 import os.path
763 from random import randint
764 from StringIO import StringIO
765+import sys
766 from textwrap import dedent
767 from threading import local
768+from types import InstanceType
769
770 import pytz
771
772 from twisted.python.util import mergeFunctionMetadata
773
774 from zope.component import ComponentLookupError, getUtility
775-from zope.security.proxy import removeSecurityProxy
776+from zope.security.proxy import (
777+ builtin_isinstance, Proxy, ProxyFactory, removeSecurityProxy)
778
779 from canonical.autodecorate import AutoDecorate
780 from canonical.config import config
781@@ -152,7 +158,6 @@
782 ANONYMOUS,
783 login,
784 login_as,
785- logout,
786 run_with_login,
787 temp_dir,
788 time_counter,
789@@ -325,7 +330,7 @@
790 branch_id, rcstype, url, cvs_root, cvs_module)
791
792
793-class LaunchpadObjectFactory(ObjectFactory):
794+class BareLaunchpadObjectFactory(ObjectFactory):
795 """Factory methods for creating Launchpad objects.
796
797 All the factory methods should be callable with no parameters.
798@@ -356,7 +361,7 @@
799
800 location = PackageLocation(copy_archive, distribution, distroseries,
801 pocket)
802- return location
803+ return ProxyFactory(location)
804
805 def makeAccount(self, displayname, email=None, password=None,
806 status=AccountStatus.ACTIVE,
807@@ -744,8 +749,8 @@
808 # We don't want to login() as the person used to create the product,
809 # so we remove the security proxy before creating the series.
810 naked_product = removeSecurityProxy(product)
811- return naked_product.newSeries(owner=owner, name=name,
812- summary=summary)
813+ return ProxyFactory(
814+ naked_product.newSeries(owner=owner, name=name, summary=summary))
815
816 def makeProject(self, name=None, displayname=None, title=None,
817 homepageurl=None, summary=None, owner=None,
818@@ -828,7 +833,7 @@
819 url = self.getUniqueURL()
820 else:
821 raise UnknownBranchTypeError(
822- 'Unrecognized branch type: %r' % (branch_type,))
823+ 'Unrecognized branch type: %r' % (branch_type, ))
824
825 namespace = get_branch_namespace(
826 owner, product=product, distroseries=distroseries,
827@@ -1034,7 +1039,7 @@
828 CodeReviewNotificationLevel.NOEMAIL, subscribed_by)
829
830 def makeDiff(self, diff_text=DIFF):
831- return Diff.fromFile(StringIO(diff_text), len(diff_text))
832+ return ProxyFactory(Diff.fromFile(StringIO(diff_text), len(diff_text)))
833
834 def makePreviewDiff(self, conflicts=u''):
835 diff = self.makeDiff()
836@@ -1614,6 +1619,7 @@
837 return series
838
839 def makeLanguage(self, language_code=None, name=None):
840+ """Makes a language given the language_code and name."""
841 if language_code is None:
842 language_code = self.getUniqueString('lang')
843 if name is None:
844@@ -1680,7 +1686,7 @@
845 description=self.getUniqueString(),
846 parent_series=parent_series, owner=distribution.owner)
847 series.status = status
848- return series
849+ return ProxyFactory(series)
850
851 # Most people think of distro releases as distro series.
852 makeDistroSeries = makeDistroRelease
853@@ -1789,7 +1795,7 @@
854
855 def makeRecipeText(self, *branches):
856 if len(branches) == 0:
857- branches = (self.makeAnyBranch(),)
858+ branches = (self.makeAnyBranch(), )
859 base_branch = branches[0]
860 other_branches = branches[1:]
861 text = MINIMAL_RECIPE_TEXT % base_branch.bzr_identity
862@@ -1952,7 +1958,7 @@
863 productseries = self.makeProductSeries(owner=owner)
864 # Make it use Translations, otherwise there's little point
865 # to us creating a template for it.
866- productseries.product.official_rosetta = True
867+ removeSecurityProxy(productseries).product.official_rosetta = True
868 templateset = getUtility(IPOTemplateSet)
869 subset = templateset.getSubset(
870 distroseries, sourcepackagename, productseries)
871@@ -2727,3 +2733,73 @@
872 new_uuid = getUtility(ITemporaryStorageManager).new(blob, expires)
873
874 return getUtility(ITemporaryStorageManager).fetch(new_uuid)
875+
876+
877+# Some factory methods return simple Python types. We don't add
878+# security wrappers for them, as well as for objects created by
879+# other Python libraries.
880+unwrapped_types = (
881+ DSCFile, InstanceType, Message, datetime, int, str, unicode)
882+
883+def is_security_proxied_or_harmless(obj):
884+ """Check that the given object is security wrapped or a harmless object."""
885+ if obj is None:
886+ return True
887+ if builtin_isinstance(obj, Proxy):
888+ return True
889+ if type(obj) in unwrapped_types:
890+ return True
891+ if isSequenceType(obj):
892+ for element in obj:
893+ if not is_security_proxied_or_harmless(element):
894+ return False
895+ return True
896+ return False
897+
898+
899+class LaunchpadObjectFactory:
900+ """A wrapper around `BareLaunchpadObjectFactory`.
901+
902+ Ensure that each object created by a `BareLaunchpadObjectFactory` method
903+ is either of a simple Python type or is security proxied.
904+
905+ A warning message is printed to stderr if a factory method creates
906+ an object without a security proxy.
907+
908+ Whereever you see such a warning: fix it!
909+ """
910+ def __init__(self):
911+ self._factory = BareLaunchpadObjectFactory()
912+
913+ def __getattr__(self, name):
914+ attr = getattr(self._factory, name)
915+ if callable(attr):
916+
917+ def guarded_method(*args, **kw):
918+ result = attr(*args, **kw)
919+ if not is_security_proxied_or_harmless(result):
920+ message = (
921+ "PLEASE FIX: LaunchpadObjectFactory.%s returns an "
922+ "unproxied object." % name)
923+ print >>sys.stderr, message
924+ return result
925+ return guarded_method
926+ else:
927+ return attr
928+
929+
930+def remove_security_proxy_and_shout_at_engineer(obj):
931+ """Remove an object's security proxy and print a warning.
932+
933+ A number of LaunchpadObjectFactory methods returned objects without
934+ a security proxy. This is now no longer possible, but a number of
935+ tests rely on unrestricted access to object attributes.
936+
937+ This function should only be used in legacy tests which fail because
938+ they expect unproxied objects.
939+ """
940+ print >>sys.stderr, (
941+ "\nWarning: called removeSecurityProxy() for %r without a check if "
942+ "this reasonable. Look for a call of "
943+ "remove_security_proxy_and_shout_at_engineer(some_object)." % obj)
944+ return removeSecurityProxy(obj)
945
946=== modified file 'lib/lp/testing/tests/test_factory.py'
947--- lib/lp/testing/tests/test_factory.py 2010-07-23 12:49:10 +0000
948+++ lib/lp/testing/tests/test_factory.py 2010-07-23 12:49:25 +0000
949@@ -8,12 +8,14 @@
950 import unittest
951
952 from zope.component import getUtility
953+from zope.security.proxy import removeSecurityProxy
954
955 from canonical.launchpad.webapp.interfaces import ILaunchBag
956 from canonical.testing.layers import DatabaseFunctionalLayer
957 from lp.code.enums import CodeImportReviewStatus
958 from lp.testing import TestCaseWithFactory
959 from lp.services.worlddata.interfaces.language import ILanguage
960+from lp.testing.factory import is_security_proxied_or_harmless
961
962
963 class TestFactory(TestCaseWithFactory):
964@@ -66,6 +68,54 @@
965 self.assertIsNot(None, person)
966 self.assertEqual(person, current_person)
967
968+ def test_is_security_proxied_or_harmless__none(self):
969+ # is_security_proxied_or_harmless() considers the None object
970+ # to be a harmless object.
971+ self.assertTrue(is_security_proxied_or_harmless(None))
972+
973+ def test_is_security_proxied_or_harmless__int(self):
974+ # is_security_proxied_or_harmless() considers integers
975+ # to be harmless.
976+ self.assertTrue(is_security_proxied_or_harmless(1))
977+
978+ def test_is_security_proxied_or_harmless__string(self):
979+ # is_security_proxied_or_harmless() considers strings
980+ # to be harmless.
981+ self.assertTrue(is_security_proxied_or_harmless('abc'))
982+
983+ def test_is_security_proxied_or_harmless__unicode(self):
984+ # is_security_proxied_or_harmless() considers unicode objects
985+ # to be harmless.
986+ self.assertTrue(is_security_proxied_or_harmless(u'abc'))
987+
988+ def test_is_security_proxied_or_harmless__proxied_object(self):
989+ # is_security_proxied_or_harmless() treats security proxied
990+ # objects as harmless.
991+ proxied_person = self.factory.makePerson()
992+ self.assertTrue(is_security_proxied_or_harmless(proxied_person))
993+
994+ def test_is_security_proxied_or_harmless__unproxied_object(self):
995+ # is_security_proxied_or_harmless() treats security proxied
996+ # objects as harmless.
997+ unproxied_person = removeSecurityProxy(self.factory.makePerson())
998+ self.assertFalse(is_security_proxied_or_harmless(unproxied_person))
999+
1000+ def test_is_security_proxied_or_harmless__sequence_harmless_content(self):
1001+ # is_security_proxied_or_harmless() checks all elements
1002+ # of a sequence. If all elements are harmless, so is the
1003+ # sequence.
1004+ proxied_person = self.factory.makePerson()
1005+ self.assertTrue(
1006+ is_security_proxied_or_harmless([1, '2', proxied_person]))
1007+
1008+ def test_is_security_proxied_or_harmless__sequence_harmful_content(self):
1009+ # is_security_proxied_or_harmless() checks all elements
1010+ # of a sequence. If at least one element is harmful, so is the
1011+ # sequence.
1012+ unproxied_person = removeSecurityProxy(self.factory.makePerson())
1013+ self.assertFalse(
1014+ is_security_proxied_or_harmless([1, '2', unproxied_person]))
1015+
1016
1017 def test_suite():
1018 return unittest.TestLoader().loadTestsFromName(__name__)
1019
1020=== modified file 'lib/lp/translations/browser/configure.zcml'
1021--- lib/lp/translations/browser/configure.zcml 2010-07-23 12:49:10 +0000
1022+++ lib/lp/translations/browser/configure.zcml 2010-07-23 12:49:25 +0000
1023@@ -269,7 +269,7 @@
1024 <browser:url
1025 for="lp.translations.interfaces.productserieslanguage.IProductSeriesLanguage"
1026 path_expression="string:+lang/${language/code}"
1027- attribute_to_parent="productseries"
1028+ attribute_to_parent="parent"
1029 rootsite="translations"/>
1030 <browser:navigation
1031 module="lp.translations.browser.serieslanguage"
1032
1033=== modified file 'lib/lp/translations/browser/serieslanguage.py'
1034--- lib/lp/translations/browser/serieslanguage.py 2010-03-04 07:31:38 +0000
1035+++ lib/lp/translations/browser/serieslanguage.py 2010-07-23 12:49:25 +0000
1036@@ -29,7 +29,7 @@
1037
1038
1039 class BaseSeriesLanguageView(LaunchpadView):
1040- """View base class to render translation status for an
1041+ """View base class to render translation status for an
1042 `IDistroSeries` and `IProductSeries`
1043
1044 This class should not be directly instantiated.
1045@@ -46,12 +46,15 @@
1046 self.translationgroup = translationgroup
1047 self.form = self.request.form
1048
1049- self.batchnav = BatchNavigator(
1050- self.series.getCurrentTranslationTemplates(),
1051- self.request)
1052-
1053- self.pofiles = self.context.getPOFilesFor(
1054- self.batchnav.currentBatch())
1055+ if IDistroSeriesLanguage.providedBy(self.context):
1056+ self.batchnav = BatchNavigator(
1057+ self.series.getCurrentTranslationTemplates(),
1058+ self.request)
1059+ self.pofiles = self.context.getPOFilesFor(
1060+ self.batchnav.currentBatch())
1061+ else:
1062+ self.batchnav = BatchNavigator(self.context.pofiles, self.request)
1063+ self.pofiles = self.batchnav.currentBatch()
1064
1065 @property
1066 def translation_group(self):
1067@@ -77,7 +80,7 @@
1068 @property
1069 def access_level_description(self):
1070 """Must not be called when there's no translation group."""
1071-
1072+
1073 if is_read_only():
1074 return (
1075 "No work can be done on these translations while Launchpad "
1076
1077=== modified file 'lib/lp/translations/browser/tests/test_breadcrumbs.py'
1078--- lib/lp/translations/browser/tests/test_breadcrumbs.py 2010-07-19 15:31:57 +0000
1079+++ lib/lp/translations/browser/tests/test_breadcrumbs.py 2010-07-23 12:49:25 +0000
1080@@ -9,6 +9,7 @@
1081
1082 from lp.services.worlddata.interfaces.language import ILanguageSet
1083 from lp.testing.breadcrumbs import BaseBreadcrumbTestCase
1084+from lp.testing.factory import remove_security_proxy_and_shout_at_engineer
1085 from lp.translations.interfaces.distroserieslanguage import (
1086 IDistroSeriesLanguageSet)
1087 from lp.translations.interfaces.productserieslanguage import (
1088@@ -109,7 +110,8 @@
1089 name='crumb-tester', displayname="Crumb Tester")
1090 series = self.factory.makeDistroRelease(
1091 name="test", version="1.0", distribution=distribution)
1092- series.hide_all_translations = False
1093+ naked_series = remove_security_proxy_and_shout_at_engineer(series)
1094+ naked_series.hide_all_translations = False
1095 serieslanguage = getUtility(IDistroSeriesLanguageSet).getDummy(
1096 series, self.language)
1097
1098
1099=== modified file 'lib/lp/translations/configure.zcml'
1100--- lib/lp/translations/configure.zcml 2010-07-23 12:49:10 +0000
1101+++ lib/lp/translations/configure.zcml 2010-07-23 12:49:25 +0000
1102@@ -399,6 +399,16 @@
1103 interface="lp.translations.interfaces.productserieslanguage.IProductSeriesLanguageSet"/>
1104 </securedutility>
1105
1106+ <!-- TranslatedLanguage -->
1107+ <facet
1108+ facet="translations">
1109+ <class
1110+ class="lp.translations.model.translatedlanguage.POFilesByPOTemplates">
1111+ <allow
1112+ interface="lp.translations.interfaces.translatedlanguage.IPOFilesByPOTemplates"/>
1113+ </class>
1114+ </facet>
1115+
1116 <!-- POTemplate -->
1117 <facet
1118 facet="translations">
1119@@ -430,6 +440,13 @@
1120 provides="lp.translations.interfaces.translationcommonformat.ITranslationFileData"
1121 factory="lp.translations.model.potemplate.POTemplateToTranslationFileDataAdapter"/>
1122
1123+ <!-- TranslationTemplatesCollection -->
1124+ <class
1125+ class="lp.translations.model.potemplate.TranslationTemplatesCollection">
1126+ <allow
1127+ interface="lp.translations.interfaces.potemplate.ITranslationTemplatesCollection"/>
1128+ </class>
1129+
1130 <!-- POTemplateSet -->
1131
1132 <securedutility
1133
1134=== modified file 'lib/lp/translations/doc/translations-export-to-branch.txt'
1135--- lib/lp/translations/doc/translations-export-to-branch.txt 2010-06-16 07:22:57 +0000
1136+++ lib/lp/translations/doc/translations-export-to-branch.txt 2010-07-23 12:49:25 +0000
1137@@ -220,8 +220,12 @@
1138 >>> from email import message_from_string
1139 >>> from lp.services.mail import stub
1140 >>> from lp.codehosting.vfs import get_rw_server
1141+ >>> from lp.testing.factory import (
1142+ ... remove_security_proxy_and_shout_at_engineer)
1143 >>> productseries = factory.makeProductSeries()
1144- >>> productseries.translations_branch = factory.makeBranch()
1145+ >>> naked_productseries = remove_security_proxy_and_shout_at_engineer(
1146+ ... productseries)
1147+ >>> naked_productseries.translations_branch = factory.makeBranch()
1148 >>> template = factory.makePOTemplate(productseries=productseries)
1149 >>> potmsgset = factory.makePOTMsgSet(template)
1150 >>> pofile = removeSecurityProxy(
1151
1152=== modified file 'lib/lp/translations/interfaces/potemplate.py'
1153--- lib/lp/translations/interfaces/potemplate.py 2010-07-23 12:49:10 +0000
1154+++ lib/lp/translations/interfaces/potemplate.py 2010-07-23 12:49:25 +0000
1155@@ -781,5 +781,19 @@
1156 exist for it.
1157 """
1158
1159+class ITranslationTemplatesCollection(Interface):
1160+ """A `Collection` of `POTemplate`s."""
1161+
1162+ def joinOuterPOFile(language=None):
1163+ """Outer-join `POFile` into the collection.
1164+
1165+ :return: A `TranslationTemplatesCollection` with an added outer
1166+ join to `POFile`.
1167+ """
1168+
1169+ def select(*args):
1170+ """Return a ResultSet for this collection with values set to args."""
1171+
1172+
1173 # Monkey patch for circular import avoidance done in
1174 # _schema_circular_imports.py
1175
1176=== modified file 'lib/lp/translations/interfaces/productserieslanguage.py'
1177--- lib/lp/translations/interfaces/productserieslanguage.py 2010-07-19 15:31:57 +0000
1178+++ lib/lp/translations/interfaces/productserieslanguage.py 2010-07-23 12:49:25 +0000
1179@@ -5,13 +5,13 @@
1180
1181 from lazr.restful.fields import Reference
1182
1183-from zope.interface import Attribute, Interface
1184-from zope.schema import (
1185- Choice, Datetime, TextLine)
1186+from zope.interface import Interface
1187+from zope.schema import Choice, TextLine
1188
1189 from canonical.launchpad import _
1190 from lp.translations.interfaces.pofile import IPOFile
1191 from lp.translations.interfaces.rosettastats import IRosettaStats
1192+from lp.translations.interfaces.translatedlanguage import ITranslatedLanguage
1193
1194 __metaclass__ = type
1195
1196@@ -21,13 +21,9 @@
1197 ]
1198
1199
1200-class IProductSeriesLanguage(IRosettaStats):
1201+class IProductSeriesLanguage(IRosettaStats, ITranslatedLanguage):
1202 """Per-language statistics for a product series."""
1203
1204- language = Choice(
1205- title=_('Language to gather statistics for.'),
1206- vocabulary='Language', required=True, readonly=True)
1207-
1208 pofile = Reference(
1209 title=_("A POFile if there is only one POTemplate for the series."),
1210 schema=IPOFile, required=False, readonly=True)
1211@@ -41,27 +37,6 @@
1212 title=_("Title for the per-language per-series page."),
1213 required=False)
1214
1215- pofiles = Attribute("The set of pofiles in this distroseries for this "
1216- "language. This includes only the real pofiles where translations "
1217- "exist.")
1218-
1219-
1220- last_changed_date = Datetime(
1221- title=_('When this file was last changed.'))
1222-
1223- def getPOFilesFor(potemplates):
1224- """Return `POFiles` for each of `potemplates`, in the same order.
1225-
1226- For any `POTemplate` that does not have a translation to the
1227- required language, a `DummyPOFile` is provided.
1228- """
1229-
1230- def setCounts(total, imported, changed, new, unreviewed, last_changed):
1231- """Set aggregated message counts for ProductSeriesLanguage."""
1232-
1233- def recalculateCounts(total, imported, changed, new, unreviewed):
1234- """Recalculate message counts for this ProductSeriesLanguage."""
1235-
1236
1237 class IProductSeriesLanguageSet(Interface):
1238 """The set of productserieslanguages."""
1239
1240=== added file 'lib/lp/translations/interfaces/translatedlanguage.py'
1241--- lib/lp/translations/interfaces/translatedlanguage.py 1970-01-01 00:00:00 +0000
1242+++ lib/lp/translations/interfaces/translatedlanguage.py 2010-07-23 12:49:25 +0000
1243@@ -0,0 +1,79 @@
1244+# Copyright 2009 Canonical Ltd. This software is licensed under the
1245+# GNU Affero General Public License version 3 (see the file LICENSE).
1246+
1247+# pylint: disable-msg=E0211,E0213
1248+
1249+from zope.interface import Attribute, Interface
1250+from zope.interface.common.sequence import IFiniteSequence
1251+from zope.schema import Datetime, Object
1252+
1253+from canonical.launchpad import _
1254+from lp.services.worlddata.interfaces.language import ILanguage
1255+from lp.translations.interfaces.potemplate import IHasTranslationTemplates
1256+from lp.registry.interfaces.person import IPerson
1257+
1258+__metaclass__ = type
1259+
1260+__all__ = [
1261+ 'IPOFilesByPOTemplates',
1262+ 'ITranslatedLanguage',
1263+ ]
1264+
1265+
1266+class ITranslatedLanguage(Interface):
1267+ """Interface for providing translations for context by language.
1268+
1269+ It expects `parent` to provide `IHasTranslationTemplates`.
1270+ """
1271+
1272+ language = Object(
1273+ title=_('Language to gather statistics and POFiles for.'),
1274+ schema=ILanguage)
1275+
1276+ parent = Object(
1277+ title=_('A parent with translation templates.'),
1278+ schema=IHasTranslationTemplates)
1279+
1280+ pofiles = Attribute(
1281+ _('Iterator over all POFiles for this context and language.'))
1282+
1283+ translation_statistics = Attribute(
1284+ _('A dict containing relevant aggregated statistics counts.'))
1285+
1286+ def setCounts(total, translated, new, changed, unreviewed):
1287+ """Set aggregated message counts for ITranslatedLanguage."""
1288+
1289+ def recalculateCounts():
1290+ """Recalculate message counts for this ITranslatedLanguage."""
1291+
1292+ last_changed_date = Datetime(
1293+ title=_('When was this translation last changed.'),
1294+ readonly=False, required=True)
1295+
1296+ last_translator = Object(
1297+ title=_('Last person that translated something in this context.'),
1298+ schema=IPerson)
1299+
1300+
1301+class IPOFilesByPOTemplates(IFiniteSequence):
1302+ """Iterate `IPOFile`s for (`ILanguage`, `ITranslationTemplateCollection`).
1303+
1304+ This is a wrapper for Storm ResultSet that enables optimized slicing
1305+ by doing it lazily on the query, thus allowing DummyPOFile objects
1306+ to be returned while still not doing more than one database query.
1307+
1308+ It subclasses `IFiniteSequence` so it can easily be used with the
1309+ BatchNavigator.
1310+ """
1311+
1312+ def __getitem__(selector):
1313+ """Get an element or slice of `IPOFile`s for given templates."""
1314+
1315+ def __getslice__(start, end):
1316+ """Deprecated, and implemented through __getitem__."""
1317+
1318+ def __iter__():
1319+ """Iterates over all `IPOFile`s for given templates."""
1320+
1321+ def __len__():
1322+ """Provides count of `IPOTemplate`s in a template collection."""
1323
1324=== modified file 'lib/lp/translations/model/potemplate.py'
1325--- lib/lp/translations/model/potemplate.py 2010-07-23 12:49:10 +0000
1326+++ lib/lp/translations/model/potemplate.py 2010-07-23 12:49:25 +0000
1327@@ -1565,7 +1565,8 @@
1328 @property
1329 def has_current_translation_templates(self):
1330 """See `IHasTranslationTemplates`."""
1331- return bool(self.getCurrentTranslationTemplates(just_ids=True).any())
1332+ return bool(
1333+ self.getCurrentTranslationTemplates(just_ids=True).any())
1334
1335 def getCurrentTranslationFiles(self, just_ids=False):
1336 """See `IHasTranslationTemplates`."""
1337@@ -1660,10 +1661,16 @@
1338 """
1339 return self.joinInner(POFile, POTemplate.id == POFile.potemplateID)
1340
1341- def joinOuterPOFile(self):
1342+ def joinOuterPOFile(self, language=None):
1343 """Outer-join `POFile` into the collection.
1344
1345 :return: A `TranslationTemplatesCollection` with an added outer
1346 join to `POFile`.
1347 """
1348- return self.joinOuter(POFile, POTemplate.id == POFile.potemplateID)
1349+ if language is not None:
1350+ return self.joinOuter(
1351+ POFile, And(POTemplate.id == POFile.potemplateID,
1352+ POFile.languageID == language.id))
1353+ else:
1354+ return self.joinOuter(
1355+ POFile, POTemplate.id == POFile.potemplateID)
1356
1357=== modified file 'lib/lp/translations/model/productserieslanguage.py'
1358--- lib/lp/translations/model/productserieslanguage.py 2010-07-19 15:38:51 +0000
1359+++ lib/lp/translations/model/productserieslanguage.py 2010-07-23 12:49:25 +0000
1360@@ -12,17 +12,13 @@
1361
1362 from zope.interface import implements
1363
1364-from storm.expr import Coalesce, Sum
1365-from storm.store import Store
1366-
1367 from lp.translations.utilities.rosettastats import RosettaStats
1368-from lp.translations.model.pofile import POFile
1369-from lp.translations.model.potemplate import get_pofiles_for, POTemplate
1370+from lp.translations.model.translatedlanguage import TranslatedLanguageMixin
1371 from lp.translations.interfaces.productserieslanguage import (
1372 IProductSeriesLanguage, IProductSeriesLanguageSet)
1373
1374
1375-class ProductSeriesLanguage(RosettaStats):
1376+class ProductSeriesLanguage(RosettaStats, TranslatedLanguageMixin):
1377 """See `IProductSeriesLanguage`."""
1378 implements(IProductSeriesLanguage)
1379
1380@@ -30,56 +26,14 @@
1381 assert 'en' != language.code, (
1382 'English is not a translatable language.')
1383 RosettaStats.__init__(self)
1384+ TranslatedLanguageMixin.__init__(self)
1385 self.productseries = productseries
1386+ self.parent = productseries
1387 self.language = language
1388 self.variant = variant
1389 self.pofile = pofile
1390 self.id = 0
1391- self._last_changed_date = None
1392-
1393- # Reset all cached counts.
1394- self.setCounts()
1395-
1396- def setCounts(self, total=0, imported=0, changed=0, new=0,
1397- unreviewed=0, last_changed=None):
1398- """See `IProductSeriesLanguage`."""
1399- self._messagecount = total
1400- # "currentcount" in RosettaStats conflicts our recent terminology
1401- # and is closer to "imported" (except that it doesn't include
1402- # "changed") translations.
1403- self._currentcount = imported
1404- self._updatescount = changed
1405- self._rosettacount = new
1406- self._unreviewed_count = unreviewed
1407- if last_changed is not None:
1408- self._last_changed_date = last_changed
1409-
1410- def _getMessageCount(self):
1411- store = Store.of(self.language)
1412- query = store.find(Sum(POTemplate.messagecount),
1413- POTemplate.productseries==self.productseries,
1414- POTemplate.iscurrent==True)
1415- total, = query
1416- if total is None:
1417- total = 0
1418- return total
1419-
1420- def recalculateCounts(self):
1421- """See `IProductSeriesLanguage`."""
1422- store = Store.of(self.language)
1423- query = store.find(
1424- (Coalesce(Sum(POFile.currentcount), 0),
1425- Coalesce(Sum(POFile.updatescount), 0),
1426- Coalesce(Sum(POFile.rosettacount), 0),
1427- Coalesce(Sum(POFile.unreviewed_count), 0)),
1428- POFile.language==self.language,
1429- POFile.variant==None,
1430- POFile.potemplate==POTemplate.id,
1431- POTemplate.productseries==self.productseries,
1432- POTemplate.iscurrent==True)
1433- imported, changed, new, unreviewed = query[0]
1434- self.setCounts(self._getMessageCount(), imported, changed,
1435- new, unreviewed)
1436+ self.last_changed_date = None
1437
1438 @property
1439 def title(self):
1440@@ -90,46 +44,29 @@
1441 self.productseries.displayname)
1442
1443 def messageCount(self):
1444- """See `IProductSeriesLanguage`."""
1445- return self._messagecount
1446+ """See `IRosettaStats`."""
1447+ return self._translation_statistics['total_count']
1448
1449 def currentCount(self, language=None):
1450- """See `IProductSeriesLanguage`."""
1451- return self._currentcount
1452+ """See `IRosettaStats`."""
1453+ translated = self._translation_statistics['translated_count']
1454+ current = translated - self.rosettaCount(language)
1455+ return current
1456
1457 def updatesCount(self, language=None):
1458- """See `IProductSeriesLanguage`."""
1459- return self._updatescount
1460+ """See `IRosettaStats`."""
1461+ return self._translation_statistics['changed_count']
1462
1463 def rosettaCount(self, language=None):
1464- """See `IProductSeriesLanguage`."""
1465- return self._rosettacount
1466+ """See `IRosettaStats`."""
1467+ new = self._translation_statistics['new_count']
1468+ changed = self._translation_statistics['changed_count']
1469+ rosetta = new + changed
1470+ return rosetta
1471
1472 def unreviewedCount(self):
1473- """See `IProductSeriesLanguage`."""
1474- return self._unreviewed_count
1475-
1476- @property
1477- def last_changed_date(self):
1478- """See `IProductSeriesLanguage`."""
1479- return self._last_changed_date
1480-
1481- @property
1482- def pofiles(self):
1483- """See `IProductSeriesLanguage`."""
1484- store = Store.of(self.language)
1485- result = store.find(
1486- POFile,
1487- POFile.language==self.language,
1488- POFile.variant==self.variant,
1489- POFile.potemplate==POTemplate.id,
1490- POTemplate.productseries==self.productseries,
1491- POTemplate.iscurrent==True)
1492- return result.order_by(['-priority'])
1493-
1494- def getPOFilesFor(self, potemplates):
1495- """See `IProductSeriesLanguage`."""
1496- return get_pofiles_for(potemplates, self.language, self.variant)
1497+ """See `IRosettaStats`."""
1498+ return self._translation_statistics['unreviewed_count']
1499
1500
1501 class ProductSeriesLanguageSet:
1502
1503=== added file 'lib/lp/translations/model/translatedlanguage.py'
1504--- lib/lp/translations/model/translatedlanguage.py 1970-01-01 00:00:00 +0000
1505+++ lib/lp/translations/model/translatedlanguage.py 2010-07-23 12:49:25 +0000
1506@@ -0,0 +1,132 @@
1507+# Copyright 2010 Canonical Ltd. This software is licensed under the
1508+# GNU Affero General Public License version 3 (see the file LICENSE).
1509+
1510+__all__ = ['TranslatedLanguageMixin']
1511+
1512+import pytz
1513+
1514+from zope.interface import implements
1515+
1516+from storm.expr import Coalesce, Desc, Max, Sum
1517+
1518+from lp.translations.interfaces.potemplate import IHasTranslationTemplates
1519+from lp.translations.interfaces.translatedlanguage import (
1520+ IPOFilesByPOTemplates, ITranslatedLanguage)
1521+from lp.translations.model.pofile import POFile
1522+from lp.translations.model.potemplate import POTemplate
1523+
1524+
1525+class POFilesByPOTemplates(object):
1526+ """See `IPOFilesByPOTemplates`."""
1527+ implements(IPOFilesByPOTemplates)
1528+
1529+ def __init__(self, templates_collection, language):
1530+ self.templates_collection = templates_collection
1531+ self.language = language
1532+
1533+ def _getDummyOrPOFile(self, potemplate, pofile):
1534+ if pofile is None:
1535+ return potemplate.getDummyPOFile(self.language,
1536+ check_for_existing=False)
1537+ else:
1538+ return pofile
1539+
1540+ def _getPOTemplatesAndPOFilesResultSet(self):
1541+ current_templates = self.templates_collection
1542+ pofiles = current_templates.joinOuterPOFile(self.language)
1543+ results = pofiles.select(POTemplate, POFile).order_by(
1544+ Desc(POTemplate.priority), POTemplate.name)
1545+ return results
1546+
1547+ def _getPOFilesForResultSet(self, resultset, selector=None):
1548+ pofiles_list = []
1549+ if selector is None:
1550+ results = resultset
1551+ else:
1552+ results = resultset[selector]
1553+ for potemplate, pofile in results:
1554+ pofiles_list.append(self._getDummyOrPOFile(potemplate, pofile))
1555+ return pofiles_list
1556+
1557+ def __getitem__(self, selector):
1558+ resultset = self._getPOTemplatesAndPOFilesResultSet()
1559+ if isinstance(selector, slice):
1560+ return self._getPOFilesForResultSet(resultset, selector)
1561+ else:
1562+ potemplate, pofile = resultset[selector]
1563+ return self._getDummyOrPOFile(potemplate, pofile)
1564+
1565+ def __iter__(self):
1566+ resultset = self._getPOTemplatesAndPOFilesResultSet()
1567+ for pofile in self._getPOFilesForResultSet(resultset):
1568+ yield pofile
1569+
1570+ def __len__(self):
1571+ return self.templates_collection.select(POTemplate).count()
1572+
1573+ def __nonzero__(self):
1574+ return bool(self.templates_collection.select(POTemplate).any())
1575+
1576+
1577+class TranslatedLanguageMixin(object):
1578+ """See `ITranslatedLanguage`."""
1579+ implements(ITranslatedLanguage)
1580+
1581+ language = None
1582+ parent = None
1583+
1584+ def __init__(self):
1585+ self.setCounts(total=0, translated=0, new=0, changed=0, unreviewed=0)
1586+
1587+ @property
1588+ def pofiles(self):
1589+ """See `ITranslatedLanguage`."""
1590+ assert IHasTranslationTemplates.providedBy(self.parent), (
1591+ "Parent object should implement `IHasTranslationTemplates`.")
1592+ current_templates = self.parent.getCurrentTemplatesCollection()
1593+ return POFilesByPOTemplates(current_templates, self.language)
1594+
1595+ @property
1596+ def translation_statistics(self):
1597+ """See `ITranslatedLanguage`."""
1598+ # This is a temporary translation statistics 'object' to allow
1599+ # smoother migration from IRosettaStats to something much nicer.
1600+ return self._translation_statistics
1601+
1602+ def setCounts(self, total, translated, new, changed, unreviewed):
1603+ """See `ITranslatedLanguage`."""
1604+ untranslated = total - translated
1605+ self._translation_statistics = {
1606+ 'total_count': total,
1607+ 'translated_count': translated,
1608+ 'new_count': new,
1609+ 'changed_count': changed,
1610+ 'unreviewed_count': unreviewed,
1611+ 'untranslated_count': untranslated,
1612+ }
1613+
1614+ def recalculateCounts(self):
1615+ """See `ITranslatedLanguage`."""
1616+ templates = self.parent.getCurrentTemplatesCollection()
1617+ pofiles = templates.joinOuterPOFile(self.language)
1618+ total_count_results = list(
1619+ pofiles.select(Coalesce(Sum(POTemplate.messagecount), 0),
1620+ Coalesce(Sum(POFile.currentcount), 0),
1621+ Coalesce(Sum(POFile.updatescount), 0),
1622+ Coalesce(Sum(POFile.rosettacount), 0),
1623+ Coalesce(Sum(POFile.unreviewed_count), 0),
1624+ Max(POFile.date_changed)))
1625+ total, imported, changed, rosetta, unreviewed, date_changed = (
1626+ total_count_results[0])
1627+ translated = imported + rosetta
1628+ new = rosetta - changed
1629+ self.setCounts(total, translated, new, changed, unreviewed)
1630+
1631+ # We have to add a timezone to the otherwise naive-datetime object
1632+ # (because we've gotten it using Max() aggregate function).
1633+ if date_changed is not None:
1634+ date_changed = date_changed.replace(tzinfo=pytz.UTC)
1635+ self.last_changed_date = date_changed
1636+
1637+ last_changed_date = None
1638+ last_translator = None
1639
1640=== modified file 'lib/lp/translations/stories/buildfarm/xx-build-summary.txt'
1641--- lib/lp/translations/stories/buildfarm/xx-build-summary.txt 2010-03-16 11:09:46 +0000
1642+++ lib/lp/translations/stories/buildfarm/xx-build-summary.txt 2010-07-23 12:49:25 +0000
1643@@ -14,6 +14,8 @@
1644 ... ILibraryFileAliasSet)
1645 >>> from canonical.launchpad.scripts.logger import QuietFakeLogger
1646 >>> from lp.buildmaster.interfaces.buildqueue import IBuildQueueSet
1647+ >>> from lp.testing.factory import (
1648+ ... remove_security_proxy_and_shout_at_engineer)
1649 >>> from lp.testing.fakemethod import FakeMethod
1650 >>> from lp.translations.interfaces.translations import (
1651 ... TranslationsBranchImportMode)
1652@@ -29,12 +31,15 @@
1653
1654 >>> productseries = factory.makeProductSeries(owner=owner)
1655 >>> product = productseries.product
1656- >>> product.official_rosetta = True
1657+ >>> naked_product = remove_security_proxy_and_shout_at_engineer(product)
1658+ >>> naked_product.official_rosetta = True
1659 >>> branch = factory.makeProductBranch(product=product, owner=owner)
1660 >>> branch_url = branch.unique_name
1661
1662- >>> productseries.branch = factory.makeBranch()
1663- >>> productseries.translations_autoimport_mode = (
1664+ >>> naked_productseries = remove_security_proxy_and_shout_at_engineer(
1665+ ... productseries)
1666+ >>> naked_productseries.branch = factory.makeBranch()
1667+ >>> naked_productseries.translations_autoimport_mode = (
1668 ... TranslationsBranchImportMode.IMPORT_TEMPLATES)
1669 >>> specific_job = factory.makeTranslationTemplatesBuildJob(branch=branch)
1670 >>> buildqueue = getUtility(IBuildQueueSet).getByJob(specific_job.job)
1671
1672=== modified file 'lib/lp/translations/tests/test_productserieslanguage.py'
1673--- lib/lp/translations/tests/test_productserieslanguage.py 2010-07-21 09:35:41 +0000
1674+++ lib/lp/translations/tests/test_productserieslanguage.py 2010-07-23 12:49:25 +0000
1675@@ -173,8 +173,9 @@
1676 self.productseries, self.language)
1677 self.assertEquals(psl.messageCount(), 0)
1678
1679- # So, we need to get it through productseries.productserieslanguages.
1680- psl = self.productseries.productserieslanguages[0]
1681+ # We explicitely ask for stats to be recalculated.
1682+ psl.recalculateCounts()
1683+
1684 self.assertPSLStatistics(psl,
1685 (pofile.messageCount(),
1686 pofile.translatedCount(),
1687@@ -199,17 +200,14 @@
1688 self.setPOFileStatistics(pofile2, 1, 1, 1, 1, pofile2.date_changed)
1689
1690 psl = self.productseries.productserieslanguages[0]
1691-
1692- # The psl.last_changed_date here is a naive datetime. So, for sake of
1693- # the tests, we should make pofile2 naive when checking if it matches
1694- # the last calculated changed date, that should be the same as
1695- # pofile2, created last.
1696+ # We explicitely ask for stats to be recalculated.
1697+ psl.recalculateCounts()
1698
1699 # Total is a sum of totals in both POTemplates (10+20).
1700 # Translated is a sum of imported and rosetta translations,
1701 # which adds up as (4+3)+(1+1).
1702 self.assertPSLStatistics(psl, (30, 9, 5, 4, 3, 6,
1703- pofile2.date_changed.replace(tzinfo=None)))
1704+ pofile2.date_changed))
1705 self.assertPSLStatistics(psl, (
1706 pofile1.messageCount() + pofile2.messageCount(),
1707 pofile1.translatedCount() + pofile2.translatedCount(),
1708@@ -217,7 +215,7 @@
1709 pofile1.rosettaCount() + pofile2.rosettaCount(),
1710 pofile1.updatesCount() + pofile2.updatesCount(),
1711 pofile1.unreviewedCount() + pofile2.unreviewedCount(),
1712- pofile2.date_changed.replace(tzinfo=None)))
1713+ pofile2.date_changed))
1714
1715 def test_recalculateCounts(self):
1716 # Test that recalculateCounts works correctly.
1717@@ -236,13 +234,14 @@
1718
1719 psl = self.psl_set.getProductSeriesLanguage(self.productseries,
1720 self.language)
1721- # recalculateCounts() doesn't recalculate the last changed date.
1722+
1723 psl.recalculateCounts()
1724 # Total is a sum of totals in both POTemplates (10+20).
1725 # Translated is a sum of imported and rosetta translations,
1726 # which adds up as (1+3)+(1+1).
1727+ # recalculateCounts() recalculates even the last changed date.
1728 self.assertPSLStatistics(psl, (30, 6, 2, 4, 3, 5,
1729- None))
1730+ pofile2.date_changed))
1731
1732 def test_recalculateCounts_no_pofiles(self):
1733 # Test that recalculateCounts works correctly even when there
1734
1735=== added file 'lib/lp/translations/tests/test_translatedlanguage.py'
1736--- lib/lp/translations/tests/test_translatedlanguage.py 1970-01-01 00:00:00 +0000
1737+++ lib/lp/translations/tests/test_translatedlanguage.py 2010-07-23 12:49:25 +0000
1738@@ -0,0 +1,462 @@
1739+# Copyright 2010 Canonical Ltd. This software is licensed under the
1740+# GNU Affero General Public License version 3 (see the file LICENSE).
1741+
1742+__metaclass__ = type
1743+
1744+from zope.component import getUtility
1745+from zope.interface.verify import verifyObject
1746+from zope.security.proxy import removeSecurityProxy
1747+
1748+from lp.translations.interfaces.productserieslanguage import (
1749+ IProductSeriesLanguageSet)
1750+from lp.translations.interfaces.translatedlanguage import ITranslatedLanguage
1751+from lp.translations.model.pofile import DummyPOFile
1752+from lp.testing import TestCaseWithFactory
1753+from canonical.testing import ZopelessDatabaseLayer
1754+
1755+
1756+class TestTranslatedLanguageMixin(TestCaseWithFactory):
1757+ """Test TranslatedLanguageMixin."""
1758+
1759+ layer = ZopelessDatabaseLayer
1760+
1761+ def setUp(self):
1762+ # Create a productseries that uses translations.
1763+ TestCaseWithFactory.setUp(self)
1764+ self.productseries = self.factory.makeProductSeries()
1765+ self.productseries.product.official_rosetta = True
1766+ self.parent = self.productseries
1767+ self.psl_set = getUtility(IProductSeriesLanguageSet)
1768+ self.language = self.factory.makeLanguage('sr@test')
1769+
1770+ def getTranslatedLanguage(self, language):
1771+ return self.psl_set.getProductSeriesLanguage(self.productseries,
1772+ language)
1773+
1774+ def addPOTemplate(self, number_of_potmsgsets=0, priority=0):
1775+ potemplate = self.factory.makePOTemplate(
1776+ productseries=self.productseries)
1777+ for sequence in range(number_of_potmsgsets):
1778+ self.factory.makePOTMsgSet(potemplate, sequence=sequence+1)
1779+ removeSecurityProxy(potemplate).messagecount = number_of_potmsgsets
1780+ potemplate.priority = priority
1781+ return potemplate
1782+
1783+ def test_interface(self):
1784+ translated_language = self.getTranslatedLanguage(self.language)
1785+ self.assertTrue(verifyObject(ITranslatedLanguage,
1786+ translated_language))
1787+
1788+ def test_language(self):
1789+ translated_language = self.getTranslatedLanguage(self.language)
1790+ self.assertEqual(self.language,
1791+ translated_language.language)
1792+
1793+ def test_parent(self):
1794+ translated_language = self.getTranslatedLanguage(self.language)
1795+ self.assertEqual(self.parent,
1796+ translated_language.parent)
1797+
1798+ def test_pofiles_notemplates(self):
1799+ translated_language = self.getTranslatedLanguage(self.language)
1800+ self.assertEqual([], list(translated_language.pofiles))
1801+
1802+ def test_pofiles_template_no_pofiles(self):
1803+ translated_language = self.getTranslatedLanguage(self.language)
1804+ potemplate = self.addPOTemplate()
1805+ dummy_pofile = potemplate.getDummyPOFile(self.language)
1806+ pofiles = list(translated_language.pofiles)
1807+ self.assertEqual(1, len(pofiles))
1808+
1809+ # When there are no actual PO files, we get a DummyPOFile object
1810+ # instead.
1811+ dummy_pofile = pofiles[0]
1812+ naked_dummy = removeSecurityProxy(dummy_pofile)
1813+ self.assertEqual(DummyPOFile, type(naked_dummy))
1814+ self.assertEqual(self.language, dummy_pofile.language)
1815+ self.assertEqual(potemplate, dummy_pofile.potemplate)
1816+
1817+ # Two queries get executed when listifying
1818+ # TranslatedLanguageMixin.pofiles: a len() does a count, and
1819+ # then all POTemplates and POFiles are fetched with the other.
1820+ self.assertStatementCount(2, list, translated_language.pofiles)
1821+
1822+ def test_pofiles_template_with_pofiles(self):
1823+ translated_language = self.getTranslatedLanguage(self.language)
1824+ potemplate = self.addPOTemplate()
1825+ pofile = self.factory.makePOFile(self.language.code, potemplate)
1826+ self.assertEqual([pofile], list(translated_language.pofiles))
1827+
1828+ # Two queries get executed when listifying
1829+ # TranslatedLanguageMixin.pofiles: a len() does a count, and
1830+ # then all POTemplates and POFiles are fetched with the other.
1831+ self.assertStatementCount(2, list, translated_language.pofiles)
1832+
1833+ def test_pofiles_two_templates(self):
1834+ translated_language = self.getTranslatedLanguage(self.language)
1835+ # Two templates with different priorities so they get sorted
1836+ # appropriately.
1837+ potemplate1 = self.addPOTemplate(priority=2)
1838+ pofile1 = self.factory.makePOFile(self.language.code, potemplate1)
1839+ potemplate2 = self.addPOTemplate(priority=1)
1840+ pofile2 = self.factory.makePOFile(self.language.code, potemplate2)
1841+ self.assertEqual([pofile1, pofile2],
1842+ list(translated_language.pofiles))
1843+
1844+ # Two queries get executed when listifying
1845+ # TranslatedLanguageMixin.pofiles: a len() does a count, and
1846+ # then all POTemplates and POFiles are fetched with the other.
1847+ self.assertStatementCount(2, list, translated_language.pofiles)
1848+
1849+ def test_pofiles_two_templates_one_dummy(self):
1850+ translated_language = self.getTranslatedLanguage(self.language)
1851+ # Two templates with different priorities so they get sorted
1852+ # appropriately.
1853+ potemplate1 = self.addPOTemplate(priority=2)
1854+ pofile1 = self.factory.makePOFile(self.language.code, potemplate1)
1855+ potemplate2 = self.addPOTemplate(priority=1)
1856+ pofiles = translated_language.pofiles
1857+ self.assertEqual(pofile1, pofiles[0])
1858+ dummy_pofile = removeSecurityProxy(pofiles[1])
1859+ self.assertEqual(DummyPOFile, type(dummy_pofile))
1860+
1861+ # Two queries get executed when listifying
1862+ # TranslatedLanguageMixin.pofiles: a len() does a count, and
1863+ # then all POTemplates and POFiles are fetched with the other.
1864+ self.assertStatementCount(2, list, translated_language.pofiles)
1865+
1866+ def test_pofiles_slicing(self):
1867+ # Slicing still works, and always does the same constant number
1868+ # of queries (1).
1869+ translated_language = self.getTranslatedLanguage(self.language)
1870+ # Three templates with different priorities so they get sorted
1871+ # appropriately.
1872+ potemplate1 = self.addPOTemplate(priority=2)
1873+ pofile1 = self.factory.makePOFile(self.language.code, potemplate1)
1874+ potemplate2 = self.addPOTemplate(priority=1)
1875+ pofile2 = self.factory.makePOFile(self.language.code, potemplate2)
1876+ potemplate3 = self.addPOTemplate(priority=0)
1877+
1878+ pofiles = translated_language.pofiles[0:2]
1879+ self.assertEqual([pofile1, pofile2], list(pofiles))
1880+
1881+ # Slicing executes only a single query.
1882+ get_slice = lambda of, start, end: list(of[start:end])
1883+ self.assertStatementCount(1, get_slice,
1884+ translated_language.pofiles, 1, 3)
1885+
1886+ def test_pofiles_slicing_dummies(self):
1887+ # Slicing includes DummyPOFiles.
1888+ translated_language = self.getTranslatedLanguage(self.language)
1889+ # Three templates with different priorities so they get sorted
1890+ # appropriately.
1891+ potemplate1 = self.addPOTemplate(priority=2)
1892+ pofile1 = self.factory.makePOFile(self.language.code, potemplate1)
1893+ potemplate2 = self.addPOTemplate(priority=1)
1894+ pofile2 = self.factory.makePOFile(self.language.code, potemplate2)
1895+ potemplate3 = self.addPOTemplate(priority=0)
1896+
1897+ pofiles = translated_language.pofiles[1:3]
1898+ self.assertEqual(pofile2, pofiles[0])
1899+ dummy_pofile = removeSecurityProxy(pofiles[1])
1900+ self.assertEqual(DummyPOFile, type(dummy_pofile))
1901+
1902+ def test_statistics_empty(self):
1903+ translated_language = self.getTranslatedLanguage(self.language)
1904+
1905+ expected = {
1906+ 'total_count': 0,
1907+ 'translated_count': 0,
1908+ 'new_count': 0,
1909+ 'changed_count': 0,
1910+ 'unreviewed_count': 0,
1911+ 'untranslated_count': 0,
1912+ }
1913+ self.assertEqual(expected,
1914+ translated_language.translation_statistics)
1915+
1916+ def test_setCounts_statistics(self):
1917+ translated_language = self.getTranslatedLanguage(self.language)
1918+
1919+ total = 5
1920+ translated = 4
1921+ new = 3
1922+ changed = 2
1923+ unreviewed = 1
1924+ untranslated = total - translated
1925+
1926+ translated_language.setCounts(
1927+ total, translated, new, changed, unreviewed)
1928+
1929+ expected = {
1930+ 'total_count': total,
1931+ 'translated_count': translated,
1932+ 'new_count': new,
1933+ 'changed_count': changed,
1934+ 'unreviewed_count': unreviewed,
1935+ 'untranslated_count': untranslated,
1936+ }
1937+ self.assertEqual(expected,
1938+ translated_language.translation_statistics)
1939+
1940+ def test_recalculateCounts_empty(self):
1941+ translated_language = self.getTranslatedLanguage(self.language)
1942+
1943+ translated_language.recalculateCounts()
1944+
1945+ expected = {
1946+ 'total_count': 0,
1947+ 'translated_count': 0,
1948+ 'new_count': 0,
1949+ 'changed_count': 0,
1950+ 'unreviewed_count': 0,
1951+ 'untranslated_count': 0,
1952+ }
1953+ self.assertEqual(expected,
1954+ translated_language.translation_statistics)
1955+
1956+ def test_recalculateCounts_total_one_pofile(self):
1957+ translated_language = self.getTranslatedLanguage(self.language)
1958+ potemplate = self.addPOTemplate(number_of_potmsgsets=5)
1959+ pofile = self.factory.makePOFile(self.language.code, potemplate)
1960+
1961+ translated_language.recalculateCounts()
1962+ self.assertEqual(
1963+ 5, translated_language.translation_statistics['total_count'])
1964+
1965+ def test_recalculateCounts_total_two_pofiles(self):
1966+ translated_language = self.getTranslatedLanguage(self.language)
1967+ potemplate1 = self.addPOTemplate(number_of_potmsgsets=5)
1968+ pofile1 = self.factory.makePOFile(self.language.code, potemplate1)
1969+ potemplate2 = self.addPOTemplate(number_of_potmsgsets=3)
1970+ pofile2 = self.factory.makePOFile(self.language.code, potemplate2)
1971+
1972+ translated_language.recalculateCounts()
1973+ self.assertEqual(
1974+ 5+3, translated_language.translation_statistics['total_count'])
1975+
1976+ def test_recalculateCounts_translated_one_pofile(self):
1977+ translated_language = self.getTranslatedLanguage(self.language)
1978+ potemplate = self.addPOTemplate(number_of_potmsgsets=5)
1979+ pofile = self.factory.makePOFile(self.language.code, potemplate)
1980+ naked_pofile = removeSecurityProxy(pofile)
1981+ # translated count is current + rosetta
1982+ naked_pofile.currentcount = 3
1983+ naked_pofile.rosettacount = 1
1984+
1985+ translated_language.recalculateCounts()
1986+ self.assertEqual(
1987+ 4, translated_language.translation_statistics['translated_count'])
1988+
1989+ def test_recalculateCounts_translated_two_pofiles(self):
1990+ translated_language = self.getTranslatedLanguage(self.language)
1991+ potemplate1 = self.addPOTemplate(number_of_potmsgsets=5)
1992+ pofile1 = self.factory.makePOFile(self.language.code, potemplate1)
1993+ naked_pofile1 = removeSecurityProxy(pofile1)
1994+ # translated count is current + rosetta
1995+ naked_pofile1.currentcount = 3
1996+ naked_pofile1.rosettacount = 1
1997+
1998+ potemplate2 = self.addPOTemplate(number_of_potmsgsets=3)
1999+ pofile2 = self.factory.makePOFile(self.language.code, potemplate2)
2000+ naked_pofile2 = removeSecurityProxy(pofile2)
2001+ # translated count is current + rosetta
2002+ naked_pofile2.currentcount = 1
2003+ naked_pofile2.rosettacount = 1
2004+
2005+ translated_language.recalculateCounts()
2006+ self.assertEqual(
2007+ 6, translated_language.translation_statistics['translated_count'])
2008+
2009+ def test_recalculateCounts_changed_one_pofile(self):
2010+ translated_language = self.getTranslatedLanguage(self.language)
2011+ potemplate = self.addPOTemplate(number_of_potmsgsets=5)
2012+ pofile = self.factory.makePOFile(self.language.code, potemplate)
2013+ naked_pofile = removeSecurityProxy(pofile)
2014+ # translated count is current + rosetta
2015+ naked_pofile.updatescount = 3
2016+
2017+ translated_language.recalculateCounts()
2018+ self.assertEqual(
2019+ 3, translated_language.translation_statistics['changed_count'])
2020+
2021+ def test_recalculateCounts_changed_two_pofiles(self):
2022+ translated_language = self.getTranslatedLanguage(self.language)
2023+ potemplate1 = self.addPOTemplate(number_of_potmsgsets=5)
2024+ pofile1 = self.factory.makePOFile(self.language.code, potemplate1)
2025+ naked_pofile1 = removeSecurityProxy(pofile1)
2026+ naked_pofile1.updatescount = 3
2027+
2028+ potemplate2 = self.addPOTemplate(number_of_potmsgsets=3)
2029+ pofile2 = self.factory.makePOFile(self.language.code, potemplate2)
2030+ naked_pofile2 = removeSecurityProxy(pofile2)
2031+ naked_pofile2.updatescount = 1
2032+
2033+ translated_language.recalculateCounts()
2034+ self.assertEqual(
2035+ 4, translated_language.translation_statistics['changed_count'])
2036+
2037+ def test_recalculateCounts_new_one_pofile(self):
2038+ translated_language = self.getTranslatedLanguage(self.language)
2039+ potemplate = self.addPOTemplate(number_of_potmsgsets=5)
2040+ pofile = self.factory.makePOFile(self.language.code, potemplate)
2041+ naked_pofile = removeSecurityProxy(pofile)
2042+ # new count is rosetta - changed
2043+ naked_pofile.rosettacount = 3
2044+ naked_pofile.updatescount = 1
2045+
2046+ translated_language.recalculateCounts()
2047+ self.assertEqual(
2048+ 2, translated_language.translation_statistics['new_count'])
2049+
2050+ def test_recalculateCounts_new_two_pofiles(self):
2051+ translated_language = self.getTranslatedLanguage(self.language)
2052+ potemplate1 = self.addPOTemplate(number_of_potmsgsets=5)
2053+ pofile1 = self.factory.makePOFile(self.language.code, potemplate1)
2054+ naked_pofile1 = removeSecurityProxy(pofile1)
2055+ # new count is rosetta - changed
2056+ naked_pofile1.rosettacount = 3
2057+ naked_pofile1.updatescount = 1
2058+
2059+ potemplate2 = self.addPOTemplate(number_of_potmsgsets=3)
2060+ pofile2 = self.factory.makePOFile(self.language.code, potemplate2)
2061+ naked_pofile2 = removeSecurityProxy(pofile2)
2062+ # new count is rosetta - changed
2063+ naked_pofile2.rosettacount = 2
2064+ naked_pofile2.updatescount = 1
2065+
2066+ translated_language.recalculateCounts()
2067+ self.assertEqual(
2068+ 3, translated_language.translation_statistics['new_count'])
2069+
2070+ def test_recalculateCounts_unreviewed_one_pofile(self):
2071+ translated_language = self.getTranslatedLanguage(self.language)
2072+ potemplate = self.addPOTemplate(number_of_potmsgsets=5)
2073+ pofile = self.factory.makePOFile(self.language.code, potemplate)
2074+ naked_pofile = removeSecurityProxy(pofile)
2075+ # translated count is current + rosetta
2076+ naked_pofile.unreviewed_count = 3
2077+
2078+ translated_language.recalculateCounts()
2079+ self.assertEqual(
2080+ 3, translated_language.translation_statistics['unreviewed_count'])
2081+
2082+ def test_recalculateCounts_unreviewed_two_pofiles(self):
2083+ translated_language = self.getTranslatedLanguage(self.language)
2084+ potemplate1 = self.addPOTemplate(number_of_potmsgsets=5)
2085+ pofile1 = self.factory.makePOFile(self.language.code, potemplate1)
2086+ naked_pofile1 = removeSecurityProxy(pofile1)
2087+ naked_pofile1.unreviewed_count = 3
2088+
2089+ potemplate2 = self.addPOTemplate(number_of_potmsgsets=3)
2090+ pofile2 = self.factory.makePOFile(self.language.code, potemplate2)
2091+ naked_pofile2 = removeSecurityProxy(pofile2)
2092+ naked_pofile2.unreviewed_count = 1
2093+
2094+ translated_language.recalculateCounts()
2095+ self.assertEqual(
2096+ 4, translated_language.translation_statistics['unreviewed_count'])
2097+
2098+ def test_recalculateCounts_one_pofile(self):
2099+ translated_language = self.getTranslatedLanguage(self.language)
2100+ potemplate = self.addPOTemplate(number_of_potmsgsets=5)
2101+ pofile = self.factory.makePOFile(self.language.code, potemplate)
2102+ naked_pofile = removeSecurityProxy(pofile)
2103+ # translated count is current + rosetta
2104+ naked_pofile.currentcount = 3
2105+ naked_pofile.rosettacount = 1
2106+ # Changed count is 'updatescount' on POFile.
2107+ # It has to be lower or equal to currentcount.
2108+ naked_pofile.updatescount = 1
2109+ # new is rosettacount-updatescount.
2110+ naked_pofile.newcount = 0
2111+ naked_pofile.unreviewed_count = 3
2112+
2113+ translated_language.recalculateCounts()
2114+
2115+ expected = {
2116+ 'total_count': 5,
2117+ 'translated_count': 4,
2118+ 'new_count': 0,
2119+ 'changed_count': 1,
2120+ 'unreviewed_count': 3,
2121+ 'untranslated_count': 1,
2122+ }
2123+ self.assertEqual(expected,
2124+ translated_language.translation_statistics)
2125+
2126+ def test_recalculateCounts_two_pofiles(self):
2127+ translated_language = self.getTranslatedLanguage(self.language)
2128+
2129+ # Set up one template with a single PO file.
2130+ potemplate1 = self.addPOTemplate(number_of_potmsgsets=5)
2131+ pofile1 = self.factory.makePOFile(self.language.code, potemplate1)
2132+ naked_pofile1 = removeSecurityProxy(pofile1)
2133+ # translated count is current + rosetta
2134+ naked_pofile1.currentcount = 2
2135+ naked_pofile1.rosettacount = 2
2136+ # Changed count is 'updatescount' on POFile.
2137+ # It has to be lower or equal to currentcount.
2138+ # new is rosettacount-updatescount.
2139+ naked_pofile1.updatescount = 1
2140+ naked_pofile1.unreviewed_count = 3
2141+
2142+ # Set up second template with a single PO file.
2143+ potemplate2 = self.addPOTemplate(number_of_potmsgsets=3)
2144+ pofile2 = self.factory.makePOFile(self.language.code, potemplate2)
2145+ naked_pofile2 = removeSecurityProxy(pofile2)
2146+ # translated count is current + rosetta
2147+ naked_pofile2.currentcount = 1
2148+ naked_pofile2.rosettacount = 2
2149+ # Changed count is 'updatescount' on POFile.
2150+ # It has to be lower or equal to currentcount.
2151+ # new is rosettacount-updatescount.
2152+ naked_pofile2.updatescount = 1
2153+ naked_pofile2.unreviewed_count = 1
2154+
2155+ translated_language.recalculateCounts()
2156+
2157+ expected = {
2158+ 'total_count': 8,
2159+ 'translated_count': 7,
2160+ 'new_count': 2,
2161+ 'changed_count': 2,
2162+ 'unreviewed_count': 4,
2163+ 'untranslated_count': 1,
2164+ }
2165+ self.assertEqual(expected,
2166+ translated_language.translation_statistics)
2167+
2168+ def test_recalculateCounts_two_templates_one_translation(self):
2169+ # Make sure recalculateCounts works even if a POFile is missing
2170+ # for one of the templates.
2171+ translated_language = self.getTranslatedLanguage(self.language)
2172+
2173+ # Set up one template with a single PO file.
2174+ potemplate1 = self.addPOTemplate(number_of_potmsgsets=5)
2175+ pofile1 = self.factory.makePOFile(self.language.code, potemplate1)
2176+ naked_pofile1 = removeSecurityProxy(pofile1)
2177+ # translated count is current + rosetta
2178+ naked_pofile1.currentcount = 2
2179+ naked_pofile1.rosettacount = 2
2180+ # Changed count is 'updatescount' on POFile.
2181+ # It has to be lower or equal to currentcount.
2182+ # new is rosettacount-updatescount.
2183+ naked_pofile1.updatescount = 1
2184+ naked_pofile1.unreviewed_count = 3
2185+
2186+ # Set up second template with a single PO file.
2187+ potemplate2 = self.addPOTemplate(number_of_potmsgsets=3)
2188+
2189+ translated_language.recalculateCounts()
2190+
2191+ expected = {
2192+ 'total_count': 8,
2193+ 'translated_count': 4,
2194+ 'new_count': 1,
2195+ 'changed_count': 1,
2196+ 'unreviewed_count': 3,
2197+ 'untranslated_count': 4,
2198+ }
2199+ self.assertEqual(expected,
2200+ translated_language.translation_statistics)
2201
2202=== modified file 'lib/lp/translations/tests/test_translationtemplatescollection.py'
2203--- lib/lp/translations/tests/test_translationtemplatescollection.py 2010-07-17 16:19:38 +0000
2204+++ lib/lp/translations/tests/test_translationtemplatescollection.py 2010-07-23 12:49:25 +0000
2205@@ -205,3 +205,22 @@
2206 ]
2207 self.assertContentEqual(
2208 expected_outcome, joined.select(POTemplate, POFile))
2209+
2210+ def test_joinOuterPOFile_language(self):
2211+ trunk = self.factory.makeProduct().getSeries('trunk')
2212+ translated_template = self.factory.makePOTemplate(productseries=trunk)
2213+ untranslated_template = self.factory.makePOTemplate(
2214+ productseries=trunk)
2215+ nl = translated_template.newPOFile('nl')
2216+ de = translated_template.newPOFile('de')
2217+
2218+ collection = TranslationTemplatesCollection()
2219+ by_series = collection.restrictProductSeries(trunk)
2220+ joined = by_series.joinOuterPOFile(language=nl.language)
2221+
2222+ expected_outcome = [
2223+ (translated_template, nl),
2224+ (untranslated_template, None),
2225+ ]
2226+ self.assertContentEqual(
2227+ expected_outcome, joined.select(POTemplate, POFile))
2228
2229=== modified file 'utilities/make-lp-user'
2230--- utilities/make-lp-user 2010-04-27 19:48:39 +0000
2231+++ utilities/make-lp-user 2010-07-23 12:49:25 +0000
2232@@ -46,7 +46,6 @@
2233 from canonical.launchpad.interfaces import (
2234 IPersonSet,
2235 ISSHKeySet,
2236- SSHKeyType,
2237 TeamMembershipStatus,
2238 )
2239 from canonical.launchpad.interfaces.gpghandler import IGPGHandler
2240@@ -111,8 +110,8 @@
2241 ssh_dir = os.path.expanduser('~/.ssh')
2242 key_set = getUtility(ISSHKeySet)
2243 key_guesses = [
2244- (SSHKeyType.RSA, 'id_rsa.pub'),
2245- (SSHKeyType.DSA, 'id_dsa.pub'),
2246+ ("ssh-rsa", 'id_rsa.pub'),
2247+ ("ssh-dsa", 'id_dsa.pub'),
2248 ]
2249 for key_type, guessed_filename in key_guesses:
2250 guessed_filename = os.path.join(ssh_dir, guessed_filename)
2251@@ -125,7 +124,8 @@
2252 except (OSError, IOError):
2253 continue
2254 public_key = public_key.split()[1]
2255- key_set.new(person, key_type, public_key, 'Added by utility script.')
2256+ key_set.new(person,
2257+ "%s %s %s" % (key_type, public_key, 'Added by utility script.'))
2258 print 'Registered SSH key: %s' % (guessed_filename,)
2259
2260

Subscribers

People subscribed via source and target branches

to status/vote changes: