Merge lp:~henninge/launchpad/bug-545354-enable-sharing into lp:~launchpad/launchpad/recife

Proposed by Henning Eggers
Status: Merged
Approved by: Edwin Grubbs
Approved revision: no longer in the source branch.
Merged at revision: 9139
Proposed branch: lp:~henninge/launchpad/bug-545354-enable-sharing
Merge into: lp:~launchpad/launchpad/recife
Diff against target: 918 lines (+666/-82)
5 files modified
database/schema/security.cfg (+1/-0)
lib/lp/translations/doc/potemplate.txt (+7/-3)
lib/lp/translations/interfaces/potemplate.py (+23/-0)
lib/lp/translations/model/potemplate.py (+161/-65)
lib/lp/translations/tests/test_shared_potemplate.py (+474/-14)
To merge this branch: bzr merge lp:~henninge/launchpad/bug-545354-enable-sharing
Reviewer Review Type Date Requested Status
Edwin Grubbs (community) Approve
Review via email: mp+27123@code.launchpad.net

Commit message

Enable message sharing between products and source packages.

Description of the change

= Bug 545354 =

Enlarges the collection of sharing templates in a POTemplateSharingSubset to include templates from linked products and source packages. Actually, in some cases it does not only "enlarge" but also "shift". The old implementation had all source packages of the same name (sourcepackagename) within a distribution share templates, this new implementation makes that connection through linkage to an upstream product. Consequently, all source packages that are linked to any series of the same product will share templates - independently of the name of the source package. Usually though, the sourcepackagename will be identical. By this we will loose packages in older Ubuntu series because the Packaging table (that holds the upstream link information) used to be insufficiently populated so that not each series was properly linked. Since Maverick this has been fixed so that future series will work just fine with this.

== Pre-implementation discussion ==

We discussed some of this in Recife in March but also just recently. The latest findings can be found in my comments on the bug. One design goal was to make sure the number of SQL queries remains constant and independent of the number of templates in the collection.

== Implementation details ==

The query in _queryByProduct is best understood by looking at this little picture: http://paste.ubuntu.com/447067/ Since a POTemplate can be linked to *either* a ProductSeries *or* a SourcePackage (distroseries, sourcepackagename), these need to be left-joined.
Also in this method I used ID throughout although not all relations need to be that explicit but I found mixing those more confusing to read.

== Tests ==

A new test case was created to test getSharingPOTemplates and getOrCreateSharedPOTMsgSet. The latter is only tested briefly because it uses the first and there is no need to re-create all test conditions. The tests also make sure the number of sql statements remains constant (at 1).

./test -vvt MessageSharingProductPackage

== Lint ==

No lint left.

To post a comment you must log in.
Revision history for this message
Henning Eggers (henninge) wrote :

Seeing how many tests were affected, I'd suggest the following test command which tests all translation tests:

bin/test -vvt lp.translations

Revision history for this message
Edwin Grubbs (edwin-grubbs) wrote :
Download full text (26.3 KiB)

Hi Henning,

This looks good. I just have a few comments below.

merge-conditional

-Edwin

>=== modified file 'lib/lp/translations/model/potemplate.py'
>--- lib/lp/translations/model/potemplate.py 2010-01-12 21:29:03 +0000
>+++ lib/lp/translations/model/potemplate.py 2010-06-16 19:25:05 +0000
>@@ -184,6 +184,21 @@
>
> _uses_english_msgids = None
>
>+ @cachedproperty
>+ def _sharing_ids(self):
>+ """Return the IDs of all sharing templates including this one."""
>+ subset = getUtility(IPOTemplateSet).getSharingSubset(
>+ product=self.product,
>+ distribution=self.distribution,
>+ sourcepackagename=self.sourcepackagename)
>+ # Convert to a list for caching.
>+ result = list(subset.getSharingPOTemplateIDs(self.name))
>+ if len(result) == 0:
>+ # Always return at least the template itself.
>+ return [self.id]
>+ else:
>+ return result

Should you also make sure that self.id is in result even if len != 0?

> def __storm_invalidated__(self):
> self.clearPOFileCache()
> self._uses_english_msgids = None
>@@ -1387,45 +1425,102 @@
> package = template.sourcepackagename.name
> return (template.name, package)
>
>- def _iterate_potemplates(self, name_pattern=None):
>- """Yield all templates matching the provided argument.
>-
>- This is much like a `IPOTemplateSubset`, except it operates on
>- `Product`s and `Distribution`s rather than `ProductSeries` and
>- `DistroSeries`.
>- """
>- if self.product:
>- subsets = [
>- self.potemplateset.getSubset(productseries=series)
>- for series in self.product.series
>- ]
>+ def _queryByProduct(self, templatename_clause):
>+ """Build the query that finds POTemplates by their linked product.
>+
>+ Queries the Packaging table to find templates in linked source
>+ packages, too.
>+
>+ :param templatename_clause: A string or a storm expression to
>+ add to the where clause of the query that will select the template
>+ name.
>+ :return: A ResultSet for the query.
>+ """
>+ from lp.registry.model.productseries import ProductSeries
>+
>+ ProductSeries1 = ClassAlias(ProductSeries)
>+ origin = LeftJoin(
>+ LeftJoin(
>+ POTemplate, ProductSeries,
>+ POTemplate.productseriesID == ProductSeries.id),
>+ Join(
>+ Packaging, ProductSeries1,
>+ Packaging.productseriesID == ProductSeries1.id),
>+ And(
>+ Packaging.distroseriesID == POTemplate.distroseriesID,
>+ Packaging.sourcepackagenameID == (
>+ POTemplate.sourcepackagenameID)
>+ )
>+ )
>+ return Store.of(self.product).using(origin).find(
>+ POTemplate,
>+ And(
>+ Or(
>+ ProductSeries.productID == self.product.id,
>+ ProductSeries1.productID == self.product.id
>+ ),
>+ ...

review: Approve
Revision history for this message
Henning Eggers (henninge) wrote :
Download full text (3.2 KiB)

Am 16.06.2010 23:24, schrieb Edwin Grubbs:
> This looks good. I just have a few comments below.

Thanks for the review. I explained one away and fixed two. ;)

>> === modified file 'lib/lp/translations/model/potemplate.py'
>> --- lib/lp/translations/model/potemplate.py 2010-01-12 21:29:03 +0000
>> +++ lib/lp/translations/model/potemplate.py 2010-06-16 19:25:05 +0000
>> @@ -184,6 +184,21 @@
>>
>> _uses_english_msgids = None
>>
>> + @cachedproperty
>> + def _sharing_ids(self):
>> + """Return the IDs of all sharing templates including this one."""
>> + subset = getUtility(IPOTemplateSet).getSharingSubset(
>> + product=self.product,
>> + distribution=self.distribution,
>> + sourcepackagename=self.sourcepackagename)
>> + # Convert to a list for caching.
>> + result = list(subset.getSharingPOTemplateIDs(self.name))
>> + if len(result) == 0:
>> + # Always return at least the template itself.
>> + return [self.id]
>> + else:
>> + return result
>
>
> Should you also make sure that self.id is in result even if len != 0?

No. Actually, the len == 0 case is basically a "no sharing possible case" and
with a single id in the list no sharing happens. If we get any ids back, those
are the sharing ones and we can trust them to include all we need.

[...]

>> def groupEquivalentPOTemplates(self, name_pattern=None): """See IPOTemplateSharingSubset.""" equivalents = {}
>>
>> - for template in self._iterate_potemplates(name_pattern):
>> + if name_pattern is None:
>> + templatename_clause = "1=1"
>> + else:
>> + templatename_clause = "potemplate.name ~ '%s'" % name_pattern
>
>
> I think you should either escape the name_pattern with sqlvalues,
> or you should convert it to a storm conditional.

Yes, thank you. I fixed that by using sqlvalues. Also I replaced "1=1" with
True since we are still in storm land here. I also pulled this out into
another variant of getSharingPOTemplates and added tests for it.

>
>
>> +
>> + for template in self._queryPOTemplates(templatename_clause):
>> key = self._get_potemplate_equivalence_class(template)
>> if key not in equivalents:
>> equivalents[key] = []
>> === modified file 'lib/lp/translations/tests/test_shared_potemplate.py'
>> --- lib/lp/translations/tests/test_shared_potemplate.py 2010-05-18 14:00:10 +0000
>> +++ lib/lp/translations/tests/test_shared_potemplate.py 2010-06-16 19:25:05 +0000
>> @@ -177,5 +176,398 @@
>> self.assertEquals(potmsgset, shared_potmsgset)
>>
>>
>> +class TestMessageSharingProductPackage(TestCaseWithFactory):
>> + """Test message sharing between a product and a package.
>> +
>> + Each test uses assertStatementCount to make sure the number of SQL
>> + queries does not change. This was integrated here to avoid having
>> + a second test case just for statement counts.
>> + The current implementation is good and only needs one statement."""
>
>
> Ending triple-quotes belongs by itself on the following line, since
> this is a multiline docstring.
>

True, thanks.

...

Read more...

1=== modified file 'lib/lp/translations/interfaces/potemplate.py'
2--- lib/lp/translations/interfaces/potemplate.py 2010-06-09 07:36:35 +0000
3+++ lib/lp/translations/interfaces/potemplate.py 2010-06-18 10:16:50 +0000
4@@ -695,6 +695,17 @@
5 :return: A list of all potemplates of the same name from all series.
6 """
7
8+ def getSharingPOTemplatesRegex(name_pattern=None):
9+ """Find all sharing templates with names matching the given pattern.
10+
11+ If name_pattern is None, match is performed on the template name.
12+ Use with care as it may return all templates in a distribution!
13+
14+ :param name_pattern: A POSIX regular expression that the template
15+ is matched against.
16+ :return: A list of all potemplates matching the pattern.
17+ """
18+
19 def getSharingPOTemplateIDs(potemplate_name):
20 """Find database ids of all sharing templates of the given name.
21
22
23=== modified file 'lib/lp/translations/model/potemplate.py'
24--- lib/lp/translations/model/potemplate.py 2010-06-16 16:31:06 +0000
25+++ lib/lp/translations/model/potemplate.py 2010-06-18 11:05:58 +0000
26@@ -1506,6 +1506,16 @@
27 return self._queryPOTemplates(
28 POTemplate.name == potemplate_name)
29
30+ def getSharingPOTemplatesRegex(self, name_pattern=None):
31+ """See IPOTemplateSharingSubset."""
32+ if name_pattern is None:
33+ templatename_clause = True
34+ else:
35+ templatename_clause = (
36+ "potemplate.name ~ %s" % sqlvalues(name_pattern))
37+
38+ return self._queryPOTemplates(templatename_clause)
39+
40 def getSharingPOTemplateIDs(self, potemplate_name):
41 """See IPOTemplateSharingSubset."""
42 return self.getSharingPOTemplates(potemplate_name).values(
43@@ -1515,12 +1525,7 @@
44 """See IPOTemplateSharingSubset."""
45 equivalents = {}
46
47- if name_pattern is None:
48- templatename_clause = "1=1"
49- else:
50- templatename_clause = "potemplate.name ~ '%s'" % name_pattern
51-
52- for template in self._queryPOTemplates(templatename_clause):
53+ for template in self.getSharingPOTemplatesRegex(name_pattern):
54 key = self._get_potemplate_equivalence_class(template)
55 if key not in equivalents:
56 equivalents[key] = []
57
58=== modified file 'lib/lp/translations/tests/test_shared_potemplate.py'
59--- lib/lp/translations/tests/test_shared_potemplate.py 2010-06-09 09:16:54 +0000
60+++ lib/lp/translations/tests/test_shared_potemplate.py 2010-06-18 11:15:54 +0000
61@@ -7,6 +7,7 @@
62
63 import unittest
64
65+from storm.exceptions import DataError
66 from zope.component import getUtility
67 from zope.security.proxy import removeSecurityProxy
68
69@@ -176,13 +177,69 @@
70 self.assertEquals(potmsgset, shared_potmsgset)
71
72
73+class TestSharingPOTemplatesRegex(TestCaseWithFactory):
74+ """Isolate tests for regular expression use in SharingSubset."""
75+
76+ layer = ZopelessDatabaseLayer
77+
78+ def setUp(self):
79+ super(TestSharingPOTemplatesRegex, self).setUp()
80+ self.product = self.factory.makeProduct()
81+ self.product.official_rosetta = True
82+ self.trunk = self.product.getSeries('trunk')
83+ self.potemplateset = getUtility(IPOTemplateSet)
84+
85+ def _makeTemplates(self, names):
86+ # Create some templates with the given names.
87+ return [
88+ self.factory.makePOTemplate(productseries=self.trunk, name=name)
89+ for name in names]
90+
91+ def test_getSharingPOTemplatesRegex_baseline(self):
92+ # Baseline test.
93+ templates = self._makeTemplates(['foo', 'foo-bar', 'foo-two'])
94+ subset = self.potemplateset.getSharingSubset(product=self.product)
95+ self.assertContentEqual(
96+ templates, subset.getSharingPOTemplatesRegex('foo.*'))
97+
98+ def test_getSharingPOTemplatesRegex_not_all(self):
99+ # A template may not match.
100+ templates = self._makeTemplates(['foo', 'foo-bar', 'foo-two'])
101+ subset = self.potemplateset.getSharingSubset(product=self.product)
102+ self.assertContentEqual(
103+ templates[1:], subset.getSharingPOTemplatesRegex('foo-.*'))
104+
105+ def test_getSharingPOTemplatesRegex_all(self):
106+ # Not passing a pattern returns all templates.
107+ templates = self._makeTemplates(['foo', 'foo-bar', 'foo-two'])
108+ subset = self.potemplateset.getSharingSubset(product=self.product)
109+ self.assertContentEqual(
110+ templates, subset.getSharingPOTemplatesRegex())
111+
112+ def test_getSharingPOTemplatesRegex_robustness_quotes(self):
113+ # Quotes in the pattern can be dangerous.
114+ subset = self.potemplateset.getSharingSubset(product=self.product)
115+ self.assertContentEqual(
116+ [], subset.getSharingPOTemplatesRegex("'\""))
117+
118+ def test_getSharingPOTemplatesRegex_robustness_backslash(self):
119+ # A backslash at the end could escape enclosing quotes without
120+ # proper escaping, leading to a SyntaxError or even a successful
121+ # exploit. Instead, storm should complain about an invalid expression
122+ # by raising DataError.
123+ subset = self.potemplateset.getSharingSubset(product=self.product)
124+ self.assertRaises(
125+ DataError, list, subset.getSharingPOTemplatesRegex("foo.*\\"))
126+
127+
128 class TestMessageSharingProductPackage(TestCaseWithFactory):
129 """Test message sharing between a product and a package.
130
131 Each test uses assertStatementCount to make sure the number of SQL
132 queries does not change. This was integrated here to avoid having
133 a second test case just for statement counts.
134- The current implementation is good and only needs one statement."""
135+ The current implementation is good and only needs one statement.
136+ """
137
138 layer = ZopelessDatabaseLayer
139
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

I looked over the incremental diff at Henning's request. A few notes, mostly already discussed on IRC. I think having the separate unit test for the regex matching (which was a bit of a wildcard, ha ha) worked out really well.

These came up on IRC:
 * Backquote identifiers in docstrings: "See `IPOTemplateSharingSubset`."""
 * getSharingPOTemplatesRegex says you're getting a regex, not templates; add "By."
 * No need to create a Product and ProductSeries; create a series and use its product.

The unit tests mostly follow the pattern "create templates with names [...]; create a subset; select from subset by regex; verify output." Most of these would be one-liners if you had a method for "create templates with names [...] (parameter 1); create a subset; select from subset by regex (parameter 2); return list of names of the selected templates." The caller could then assertContentsEqual the result against a list of expected strings. That would also help with:
 * Making the output more obvious (the relevance of "templates[1:]" is less clear).
 * Testing lots of other things that could go wrong; see below.
 * Eliminating implicit state. You wouldn't need any setUp at all.

Other notes:
 * You may want to test that you're really getting a match, not a search: "xyz" should not match "y.*" As long as we're not taking the regex from user input but from existing template names, this is a bigger risk than unescaped quotes. (Not that I disagree with your approach of testing for those right from the start!)
 * "# Quotes in the pattern can be dangerous." To me that looks like you're specifying the appropriate behaviour! How about "quotes do not confuse the regex match"?
 * I'd test single and double quotes separately, not in the same string.
 * When you test for quoting robustness, you might as well set up a template to establish that you don't get any unexpected matches.
 * You don't exercise the case where no templates match.

Apart from those notes, the incremental diff has my unreserved blessing.

Jeroen

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'database/schema/security.cfg'
2--- database/schema/security.cfg 2010-05-20 13:59:11 +0000
3+++ database/schema/security.cfg 2010-06-22 07:46:30 +0000
4@@ -1341,6 +1341,7 @@
5 public.distribution = SELECT
6 public.distroseries = SELECT
7 public.language = SELECT
8+public.packaging = SELECT
9 public.person = SELECT
10 public.pofile = SELECT, INSERT, UPDATE
11 public.pomsgid = SELECT
12
13=== modified file 'lib/lp/translations/doc/potemplate.txt'
14--- lib/lp/translations/doc/potemplate.txt 2010-04-26 16:00:31 +0000
15+++ lib/lp/translations/doc/potemplate.txt 2010-06-22 07:46:30 +0000
16@@ -228,7 +228,7 @@
17
18 == getPOFileByPath ==
19
20-We can get an IPOFile inside a templage based on its path.
21+We can get an IPOFile inside a template based on its path.
22
23 >>> pofile = potemplate.getPOFileByPath('es.po')
24 >>> print pofile.title
25@@ -614,7 +614,10 @@
26 >>> print exported_translation_file.content_type
27 application/x-gtar
28
29-Inspecting the tarball content, we have the list of entries exported.
30+Inspecting the tarball content, we have the list of entries exported. This
31+includes the 'pt' POFile that was created earlier on the 'evolution'
32+product as this is sharing translations with the source package that this
33+potemplate is from.
34
35 >>> from canonical.launchpad.helpers import string_to_tarfile
36 >>> tarfile_string = exported_translation_file.read()
37@@ -625,7 +628,7 @@
38 >>> sorted(name.rstrip('/') for name in tarfile.getnames())
39 ['evolution-2.2', 'evolution-2.2/evolution-2.2-es.po',
40 'evolution-2.2/evolution-2.2-ja.po', 'evolution-2.2/evolution-2.2-xh.po',
41- 'po', 'po/evolution-2.2.pot']
42+ 'po', 'po/evolution-2.2-pt.po', 'po/evolution-2.2.pot']
43
44 The *-es.po file is indeed the Spanish translation...
45
46@@ -644,6 +647,7 @@
47 evolution-2.2/evolution-2.2-ja.po
48 evolution-2.2/evolution-2.2-xh.po
49 po/
50+ po/evolution-2.2-pt.po
51 po/evolution-2.2.pot
52 >>> pofile = simple_popen2(
53 ... ['tar', 'zxfO', '-', 'evolution-2.2/evolution-2.2-es.po'],
54
55=== modified file 'lib/lp/translations/interfaces/potemplate.py'
56--- lib/lp/translations/interfaces/potemplate.py 2009-11-27 12:50:16 +0000
57+++ lib/lp/translations/interfaces/potemplate.py 2010-06-22 07:46:30 +0000
58@@ -695,6 +695,29 @@
59 :return: A list of all potemplates of the same name from all series.
60 """
61
62+ def getSharingPOTemplatesByRegex(name_pattern=None):
63+ """Find all sharing templates with names matching the given pattern.
64+
65+ If name_pattern is None, match is performed on the template name.
66+ Use with care as it may return all templates in a distribution!
67+
68+ :param name_pattern: A POSIX regular expression that the template
69+ is matched against.
70+ :return: A list of all potemplates matching the pattern.
71+ """
72+
73+ def getSharingPOTemplateIDs(potemplate_name):
74+ """Find database ids of all sharing templates of the given name.
75+
76+ For distributions this method requires that sourcepackagename is set.
77+ This avoids serialization of full POTemplate objects.
78+
79+ :param potemplate_name: The name of the template for which to find
80+ sharing equivalents.
81+ :return: A list of database ids of all potemplates of the same name
82+ from all series.
83+ """
84+
85 def groupEquivalentPOTemplates(name_pattern=None):
86 """Within given IProduct or IDistribution, find equivalent templates.
87
88
89=== modified file 'lib/lp/translations/model/potemplate.py'
90--- lib/lp/translations/model/potemplate.py 2010-01-12 21:29:03 +0000
91+++ lib/lp/translations/model/potemplate.py 2010-06-22 07:46:30 +0000
92@@ -18,7 +18,6 @@
93 import datetime
94 import logging
95 import os
96-import re
97
98 from psycopg2.extensions import TransactionRollbackError
99 from sqlobject import (
100@@ -26,7 +25,7 @@
101 StringCol)
102 from storm.expr import Alias, And, Desc, In, Join, LeftJoin, Or, SQL
103 from storm.info import ClassAlias
104-from storm.store import Store
105+from storm.store import EmptyResultSet, Store
106 from zope.component import getAdapter, getUtility
107 from zope.interface import implements
108 from zope.security.proxy import removeSecurityProxy
109@@ -43,6 +42,7 @@
110 from lp.services.database.prejoin import prejoin
111 from lp.services.worlddata.model.language import Language
112 from lp.registry.interfaces.person import validate_public_person
113+from lp.registry.model.packaging import Packaging
114 from lp.registry.model.sourcepackagename import SourcePackageName
115 from lp.translations.model.pofile import POFile, DummyPOFile
116 from lp.translations.model.pomsgid import POMsgID
117@@ -184,6 +184,21 @@
118
119 _uses_english_msgids = None
120
121+ @cachedproperty
122+ def _sharing_ids(self):
123+ """Return the IDs of all sharing templates including this one."""
124+ subset = getUtility(IPOTemplateSet).getSharingSubset(
125+ product=self.product,
126+ distribution=self.distribution,
127+ sourcepackagename=self.sourcepackagename)
128+ # Convert to a list for caching.
129+ result = list(subset.getSharingPOTemplateIDs(self.name))
130+ if len(result) == 0:
131+ # Always return at least the template itself.
132+ return [self.id]
133+ else:
134+ return result
135+
136 def __storm_invalidated__(self):
137 self.clearPOFileCache()
138 self._uses_english_msgids = None
139@@ -339,8 +354,7 @@
140 if self.productseries is not None:
141 return self.productseries
142 elif self.distroseries is not None:
143- from lp.registry.model.sourcepackage import \
144- SourcePackage
145+ from lp.registry.model.sourcepackage import SourcePackage
146 return SourcePackage(distroseries=self.distroseries,
147 sourcepackagename=self.sourcepackagename)
148 raise AssertionError('Unknown POTemplate translation target')
149@@ -560,31 +574,10 @@
150 ]
151 clause_tables = ['TranslationTemplateItem']
152 if sharing_templates:
153- clauses.extend([
154- 'TranslationTemplateItem.potemplate = POTemplate.id',
155- 'TranslationTemplateItem.potmsgset = POTMsgSet.id',
156- 'POTemplate.name = %s' % sqlvalues(self.name),
157- ])
158- product = None
159- distribution = None
160- clause_tables.append('POTemplate')
161- if self.productseries is not None:
162- product = self.productseries.product
163- clauses.extend([
164- 'POTemplate.productseries=ProductSeries.id',
165- 'ProductSeries.product %s' % self._null_quote(product)
166- ])
167- clause_tables.append('ProductSeries')
168- elif self.distroseries is not None:
169- distribution = self.distroseries.distribution
170- clauses.extend([
171- 'POTemplate.distroseries=DistroSeries.id',
172- 'DistroSeries.distribution %s' % (
173- self._null_quote(distribution)),
174- 'POTemplate.sourcepackagename %s' % self._null_quote(
175- self.sourcepackagename),
176- ])
177- clause_tables.append('DistroSeries')
178+ clauses.append(
179+ 'TranslationTemplateItem.potemplate in %s' % sqlvalues(
180+ self._sharing_ids)
181+ )
182 else:
183 clauses.append(
184 'TranslationTemplateItem.potemplate = %s' % sqlvalues(self))
185@@ -1374,9 +1367,54 @@
186 assert distribution or not sourcepackagename, (
187 "Picking a source package only makes sense with a distribution.")
188 self.potemplateset = potemplateset
189- self.distribution = distribution
190- self.sourcepackagename = sourcepackagename
191- self.product = product
192+ self.share_by_name = False
193+ self.subset = None
194+
195+ if distribution is not None:
196+ self.distribution = distribution
197+ self.sourcepackagename = sourcepackagename
198+ if sourcepackagename is not None:
199+ product = self._findUpstreamProduct(
200+ distribution, sourcepackagename)
201+ if product is None:
202+ self.share_by_name = self._canShareByName(
203+ distribution, sourcepackagename)
204+ if product is not None:
205+ self.product = product
206+
207+ def _findUpstreamProduct(self, distribution, sourcepackagename):
208+ """Find the upstream product by looking at the translation focus.
209+
210+ The translation focus is used to pick a distroseries, so a source
211+ package instance can be created. If no translation focus is set,
212+ the distribution's current series is used."""
213+
214+ from lp.registry.model.sourcepackage import SourcePackage
215+ distroseries = distribution.translation_focus
216+ if distroseries is None:
217+ distroseries = distribution.currentseries
218+ sourcepackage = SourcePackage(sourcepackagename, distroseries)
219+ if sourcepackage.productseries is None:
220+ return None
221+ return sourcepackage.productseries.product
222+
223+ def _canShareByName(self, distribution, sourcepackagename):
224+ """Determine if sharing by sourcepackagename is a wise thing.
225+
226+ Without a product, the linkage between sharing packages can only be
227+ determined by their name. This is only (fairly) safe if none of these
228+ is packaged elsewhere.
229+ """
230+ from lp.registry.model.distroseries import DistroSeries
231+ origin = Join(
232+ Packaging, DistroSeries,
233+ Packaging.distroseries == DistroSeries.id)
234+ result = Store.of(distribution).using(origin).find(
235+ Packaging,
236+ And(DistroSeries.distribution == distribution.id,
237+ Packaging.sourcepackagename == sourcepackagename.id)
238+ ).count()
239+ return result == 0
240
241
242 def _get_potemplate_equivalence_class(self, template):
243@@ -1387,45 +1425,107 @@
244 package = template.sourcepackagename.name
245 return (template.name, package)
246
247- def _iterate_potemplates(self, name_pattern=None):
248- """Yield all templates matching the provided argument.
249-
250- This is much like a `IPOTemplateSubset`, except it operates on
251- `Product`s and `Distribution`s rather than `ProductSeries` and
252- `DistroSeries`.
253- """
254- if self.product:
255- subsets = [
256- self.potemplateset.getSubset(productseries=series)
257- for series in self.product.series
258- ]
259+ def _queryByProduct(self, templatename_clause):
260+ """Build the query that finds POTemplates by their linked product.
261+
262+ Queries the Packaging table to find templates in linked source
263+ packages, too.
264+
265+ :param templatename_clause: A string or a storm expression to
266+ add to the where clause of the query that will select the template
267+ name.
268+ :return: A ResultSet for the query.
269+ """
270+ from lp.registry.model.productseries import ProductSeries
271+
272+ ProductSeries1 = ClassAlias(ProductSeries)
273+ origin = LeftJoin(
274+ LeftJoin(
275+ POTemplate, ProductSeries,
276+ POTemplate.productseriesID == ProductSeries.id),
277+ Join(
278+ Packaging, ProductSeries1,
279+ Packaging.productseriesID == ProductSeries1.id),
280+ And(
281+ Packaging.distroseriesID == POTemplate.distroseriesID,
282+ Packaging.sourcepackagenameID == (
283+ POTemplate.sourcepackagenameID)
284+ )
285+ )
286+ return Store.of(self.product).using(origin).find(
287+ POTemplate,
288+ And(
289+ Or(
290+ ProductSeries.productID == self.product.id,
291+ ProductSeries1.productID == self.product.id
292+ ),
293+ templatename_clause
294+ ))
295+
296+ def _queryBySourcepackagename(self, templatename_clause):
297+ """Build the query that finds POTemplates by their names.
298+
299+ :param templatename_clause: A string or a storm expression to
300+ add to the where clause of the query that will select the template
301+ name.
302+ :return: A ResultSet for the query.
303+ """
304+ from lp.registry.model.distroseries import DistroSeries
305+ origin = Join(
306+ POTemplate, DistroSeries,
307+ POTemplate.distroseries == DistroSeries.id)
308+ return Store.of(self.distribution).using(origin).find(
309+ POTemplate,
310+ And(
311+ DistroSeries.distributionID == self.distribution.id,
312+ POTemplate.sourcepackagename == self.sourcepackagename,
313+ templatename_clause
314+ ))
315+
316+ def _queryByDistribution(self, templatename_clause):
317+ """Special case when templates are searched across a distribution."""
318+ return Store.of(self.distribution).find(
319+ POTemplate, templatename_clause)
320+
321+ def _queryPOTemplates(self, templatename_clause):
322+ """Select the right query to be used."""
323+ if self.product is not None:
324+ return self._queryByProduct(templatename_clause)
325+ elif self.share_by_name:
326+ return self._queryBySourcepackagename(templatename_clause)
327+ elif self.distribution is not None and self.sourcepackagename is None:
328+ return self._queryByDistribution(templatename_clause)
329 else:
330- subsets = [
331- self.potemplateset.getSubset(
332- distroseries=series,
333- sourcepackagename=self.sourcepackagename)
334- for series in self.distribution.series
335- ]
336- for subset in subsets:
337- for template in subset:
338- if name_pattern is None or re.match(name_pattern,
339- template.name):
340- yield template
341+ return EmptyResultSet()
342
343 def getSharingPOTemplates(self, potemplate_name):
344- """See IPOTemplateSharingSubset."""
345+ """See `IPOTemplateSharingSubset`."""
346 if self.distribution is not None:
347 assert self.sourcepackagename is not None, (
348 "Need sourcepackagename to select from distribution.")
349-
350- escaped_potemplate_name = re.escape(potemplate_name)
351- return self._iterate_potemplates("^%s$" % escaped_potemplate_name)
352+ return self._queryPOTemplates(
353+ POTemplate.name == potemplate_name)
354+
355+ def getSharingPOTemplatesByRegex(self, name_pattern=None):
356+ """See `IPOTemplateSharingSubset`."""
357+ if name_pattern is None:
358+ templatename_clause = True
359+ else:
360+ templatename_clause = (
361+ "potemplate.name ~ %s" % sqlvalues(name_pattern))
362+
363+ return self._queryPOTemplates(templatename_clause)
364+
365+ def getSharingPOTemplateIDs(self, potemplate_name):
366+ """See `IPOTemplateSharingSubset`."""
367+ return self.getSharingPOTemplates(potemplate_name).values(
368+ POTemplate.id)
369
370 def groupEquivalentPOTemplates(self, name_pattern=None):
371- """See IPOTemplateSharingSubset."""
372+ """See `IPOTemplateSharingSubset`."""
373 equivalents = {}
374
375- for template in self._iterate_potemplates(name_pattern):
376+ for template in self.getSharingPOTemplatesByRegex(name_pattern):
377 key = self._get_potemplate_equivalence_class(template)
378 if key not in equivalents:
379 equivalents[key] = []
380@@ -1510,10 +1610,6 @@
381 if flag
382 ])
383
384- # Store sequences so we can detect later whether we changed the
385- # message.
386- sequence = row.sequence
387-
388 # Store the message.
389 messages.append(msgset)
390
391
392=== modified file 'lib/lp/translations/tests/test_shared_potemplate.py'
393--- lib/lp/translations/tests/test_shared_potemplate.py 2010-05-18 14:00:10 +0000
394+++ lib/lp/translations/tests/test_shared_potemplate.py 2010-06-22 07:46:30 +0000
395@@ -7,13 +7,16 @@
396
397 import unittest
398
399+from storm.exceptions import DataError
400+from zope.component import getUtility
401 from zope.security.proxy import removeSecurityProxy
402
403-from lp.testing.factory import LaunchpadObjectFactory
404+from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
405 from canonical.testing import ZopelessDatabaseLayer
406-
407-
408-class TestTranslationSharingPOTemplate(unittest.TestCase):
409+from lp.testing import TestCaseWithFactory
410+from lp.translations.interfaces.potemplate import IPOTemplateSet
411+
412+class TestTranslationSharingPOTemplate(TestCaseWithFactory):
413 """Test behaviour of "sharing" PO templates."""
414
415 layer = ZopelessDatabaseLayer
416@@ -22,20 +25,19 @@
417 """Set up context to test in."""
418 # Create a product with two series and sharing POTemplates
419 # in different series ('devel' and 'stable').
420- factory = LaunchpadObjectFactory()
421- self.factory = factory
422- self.foo = factory.makeProduct()
423- self.foo_devel = factory.makeProductSeries(
424+ super(TestTranslationSharingPOTemplate, self).setUp()
425+ self.foo = self.factory.makeProduct()
426+ self.foo_devel = self.factory.makeProductSeries(
427 name='devel', product=self.foo)
428- self.foo_stable = factory.makeProductSeries(
429+ self.foo_stable = self.factory.makeProductSeries(
430 name='stable', product=self.foo)
431 self.foo.official_rosetta = True
432
433 # POTemplate is a 'sharing' one if it has the same name ('messages').
434- self.devel_potemplate = factory.makePOTemplate(
435+ self.devel_potemplate = self.factory.makePOTemplate(
436 productseries=self.foo_devel, name="messages")
437- self.stable_potemplate = factory.makePOTemplate(self.foo_stable,
438- name="messages")
439+ self.stable_potemplate = self.factory.makePOTemplate(
440+ self.foo_stable, name="messages")
441
442 # Create a single POTMsgSet that is used across all tests,
443 # and add it to only one of the POTemplates.
444@@ -106,8 +108,6 @@
445 self.assertEquals(has_message_id, True)
446
447 def test_hasPluralMessage(self):
448- naked_potemplate = removeSecurityProxy(self.devel_potemplate)
449-
450 # At the moment, a POTemplate has no plural form messages.
451 self.assertEquals(self.devel_potemplate.hasPluralMessage(), False)
452
453@@ -177,5 +177,465 @@
454 self.assertEquals(potmsgset, shared_potmsgset)
455
456
457+class TestSharingPOTemplatesByRegex(TestCaseWithFactory):
458+ """Isolate tests for regular expression use in SharingSubset."""
459+
460+ layer = ZopelessDatabaseLayer
461+
462+ def setUp(self):
463+ super(TestSharingPOTemplatesByRegex, self).setUp()
464+
465+ def _makeAndFind(self, names, name_pattern=None):
466+ product = self.factory.makeProduct()
467+ product.official_rosetta = True
468+ trunk = product.getSeries('trunk')
469+ for name in names:
470+ self.factory.makePOTemplate(productseries=trunk, name=name)
471+ subset = getUtility(IPOTemplateSet).getSharingSubset(product=product)
472+ return [
473+ template.name
474+ for template in subset.getSharingPOTemplatesByRegex(name_pattern)]
475+
476+ def test_getSharingPOTemplatesByRegex_baseline(self):
477+ # Baseline test.
478+ self.assertContentEqual(
479+ ['foo', 'foo-bar', 'foo-two'],
480+ self._makeAndFind(['foo', 'foo-bar', 'foo-two'], 'foo.*'))
481+
482+ def test_getSharingPOTemplatesByRegex_not_all(self):
483+ # A template may not match.
484+ self.assertContentEqual(
485+ ['foo-bar', 'foo-two'],
486+ self._makeAndFind(['foo', 'foo-bar', 'foo-two'], 'foo-.*'))
487+
488+ def test_getSharingPOTemplatesByRegex_all(self):
489+ # Not passing a pattern returns all templates.
490+ self.assertContentEqual(
491+ ['foo', 'foo-bar', 'foo-two'],
492+ self._makeAndFind(['foo', 'foo-bar', 'foo-two']))
493+
494+ def test_getSharingPOTemplatesByRegex_no_match(self):
495+ # A not matching pattern returns no templates.
496+ self.assertContentEqual(
497+ [],
498+ self._makeAndFind(['foo', 'foo-bar', 'foo-two'], "doo.+dle"))
499+
500+ def test_getSharingPOTemplatesByRegex_robustness_single_quotes(self):
501+ # Single quotes do not confuse the regex match.
502+ self.assertContentEqual(
503+ [],
504+ self._makeAndFind(['foo', 'foo-bar', 'foo-two'], "'"))
505+
506+ def test_getSharingPOTemplatesByRegex_robustness_double_quotes(self):
507+ # Double quotes do not confuse the regex match.
508+ self.assertContentEqual(
509+ [],
510+ self._makeAndFind(['foo', 'foo-bar', 'foo-two'], '"'))
511+
512+ def test_getSharingPOTemplatesByRegex_robustness_backslash(self):
513+ # A backslash at the end could escape enclosing quotes without
514+ # proper escaping, leading to a SyntaxError or even a successful
515+ # exploit. Instead, storm should complain about an invalid expression
516+ # by raising DataError.
517+ product = self.factory.makeProduct()
518+ subset = getUtility(IPOTemplateSet).getSharingSubset(product=product)
519+ self.assertRaises(
520+ DataError, list, subset.getSharingPOTemplatesByRegex("foo.*\\"))
521+
522+
523+class TestMessageSharingProductPackage(TestCaseWithFactory):
524+ """Test message sharing between a product and a package.
525+
526+ Each test uses assertStatementCount to make sure the number of SQL
527+ queries does not change. This was integrated here to avoid having
528+ a second test case just for statement counts.
529+ The current implementation is good and only needs one statement.
530+ """
531+
532+ layer = ZopelessDatabaseLayer
533+
534+ def setUp(self):
535+ super(TestMessageSharingProductPackage, self).setUp()
536+
537+ self.ubuntu = getUtility(ILaunchpadCelebrities).ubuntu
538+ self.hoary = self.ubuntu['hoary']
539+ self.warty = self.ubuntu['warty']
540+ self.ubuntu.translation_focus = self.hoary
541+ self.packagename = self.factory.makeSourcePackageName()
542+
543+ self.product = self.factory.makeProduct()
544+ self.trunk = self.product.getSeries('trunk')
545+ self.stable = self.factory.makeProductSeries(
546+ product=self.product)
547+
548+ self.templatename = self.factory.getUniqueString()
549+ self.trunk_template = self.factory.makePOTemplate(
550+ productseries=self.trunk, name=self.templatename)
551+ self.hoary_template = self.factory.makePOTemplate(
552+ distroseries=self.hoary, sourcepackagename=self.packagename,
553+ name=self.templatename)
554+
555+ self.owner = self.factory.makePerson()
556+ self.potemplateset = getUtility(IPOTemplateSet)
557+
558+ def _assertStatements(self, no_of_statements, resultset):
559+ """Assert constant number of SQL statements when iterating result set.
560+
561+ This encapsulates using the 'list' function to feed the iterator to
562+ the assert method. This iterates the resultset, triggering SQL
563+ statement execution."""
564+ return self.assertStatementCount(no_of_statements, list, resultset)
565+
566+ def test_getSharingPOTemplates_product(self):
567+ # Sharing templates for a product include the same templates from
568+ # a linked source package.
569+ self.factory.makeSourcePackagePublishingHistory(
570+ sourcepackagename=self.packagename,
571+ distroseries=self.hoary)
572+ self.trunk.setPackaging(self.hoary, self.packagename, self.owner)
573+ subset = self.potemplateset.getSharingSubset(product=self.product)
574+ templates = self._assertStatements(
575+ 1, subset.getSharingPOTemplates(self.templatename))
576+
577+ self.assertContentEqual(
578+ [self.trunk_template, self.hoary_template], templates)
579+
580+ def test_getSharingPOTemplates_package(self):
581+ # Sharing templates for a source package include the same templates
582+ # from a linked product.
583+ sourcepackage = self.factory.makeSourcePackage(
584+ self.packagename, self.hoary)
585+ sourcepackage.setPackaging(self.trunk, self.owner)
586+ subset = self.potemplateset.getSharingSubset(
587+ distribution=self.ubuntu,
588+ sourcepackagename=self.packagename)
589+ templates = self._assertStatements(
590+ 1, subset.getSharingPOTemplates(self.templatename))
591+
592+ self.assertContentEqual(
593+ [self.trunk_template, self.hoary_template], templates)
594+
595+ def test_getSharingPOTemplates_product_multiple_series(self):
596+ # Sharing templates for a product include the same templates from
597+ # a linked source package, even from multiple product series.
598+ # But templates for the same sourcepackagename are not returned
599+ # if they are not linked.
600+ stable_template = self.factory.makePOTemplate(
601+ productseries=self.stable, name=self.templatename)
602+ # This will not be returned.
603+ self.factory.makePOTemplate(
604+ distroseries=self.warty, sourcepackagename=self.packagename,
605+ name=self.templatename)
606+ self.factory.makeSourcePackagePublishingHistory(
607+ sourcepackagename=self.packagename,
608+ distroseries=self.hoary)
609+ self.trunk.setPackaging(self.hoary, self.packagename, self.owner)
610+ subset = self.potemplateset.getSharingSubset(product=self.product)
611+ templates = self._assertStatements(
612+ 1, subset.getSharingPOTemplates(self.templatename))
613+
614+ self.assertContentEqual(
615+ [self.trunk_template, self.hoary_template, stable_template],
616+ templates)
617+
618+ def test_getSharingPOTemplates_package_multiple_series(self):
619+ # Sharing templates for a source package include the same templates
620+ # from a linked product, even with multiple product series.
621+ # To include templates from other source packages, the product must
622+ # be linked to that one, too.
623+ stable_template = self.factory.makePOTemplate(
624+ productseries=self.stable, name=self.templatename)
625+ warty_template = self.factory.makePOTemplate(
626+ distroseries=self.warty, sourcepackagename=self.packagename,
627+ name=self.templatename)
628+ hoary_sourcepackage = self.factory.makeSourcePackage(
629+ self.packagename, self.hoary)
630+ hoary_sourcepackage.setPackaging(self.trunk, self.owner)
631+ warty_sourcepackage = self.factory.makeSourcePackage(
632+ self.packagename, self.warty)
633+ warty_sourcepackage.setPackaging(self.stable, self.owner)
634+ subset = self.potemplateset.getSharingSubset(
635+ distribution=self.ubuntu,
636+ sourcepackagename=self.packagename)
637+ templates = self._assertStatements(
638+ 1, subset.getSharingPOTemplates(self.templatename))
639+
640+ self.assertContentEqual(
641+ [self.trunk_template, self.hoary_template,
642+ stable_template, warty_template],
643+ templates)
644+
645+ def test_getSharingPOTemplates_package_name_changed(self):
646+ # When the name of a package changes (but not the name of the
647+ # template), it will still share translations if it is linked
648+ # to the same product.
649+ changed_name = self.factory.makeSourcePackageName()
650+ warty_template = self.factory.makePOTemplate(
651+ distroseries=self.warty, sourcepackagename=changed_name,
652+ name=self.templatename)
653+ hoary_sourcepackage = self.factory.makeSourcePackage(
654+ self.packagename, self.hoary)
655+ hoary_sourcepackage.setPackaging(self.trunk, self.owner)
656+ warty_sourcepackage = self.factory.makeSourcePackage(
657+ changed_name, self.warty)
658+ warty_sourcepackage.setPackaging(self.stable, self.owner)
659+ subset = self.potemplateset.getSharingSubset(
660+ distribution=self.ubuntu,
661+ sourcepackagename=self.packagename)
662+ templates = self._assertStatements(
663+ 1, subset.getSharingPOTemplates(self.templatename))
664+
665+ self.assertContentEqual(
666+ [self.trunk_template, self.hoary_template, warty_template],
667+ templates)
668+
669+ def test_getSharingPOTemplates_many_series(self):
670+ # The number of queries for a call to getSharingPOTemplates must
671+ # remain constant.
672+
673+ all_templates = [self.trunk_template, self.hoary_template]
674+ hoary_sourcepackage = self.factory.makeSourcePackage(
675+ self.packagename, self.hoary)
676+ hoary_sourcepackage.setPackaging(self.trunk, self.owner)
677+ # Add a greater number of series and sharing templates on either side.
678+ seriesnames = (
679+ ('0.1', 'feisty'),
680+ ('0.2', 'gutsy'),
681+ ('0.3', 'hardy'),
682+ ('0.4', 'intrepid'),
683+ ('0.5', 'jaunty'),
684+ ('0.6', 'karmic'),
685+ )
686+ for pseries_name, dseries_name in seriesnames:
687+ productseries = self.factory.makeProductSeries(
688+ self.product, pseries_name)
689+ all_templates.append(self.factory.makePOTemplate(
690+ productseries=productseries, name=self.templatename))
691+ distroseries = self.factory.makeDistroSeries(
692+ self.ubuntu, name=dseries_name)
693+ all_templates.append(self.factory.makePOTemplate(
694+ distroseries=distroseries, sourcepackagename=self.packagename,
695+ name=self.templatename))
696+ sourcepackage = self.factory.makeSourcePackage(
697+ self.packagename, distroseries)
698+ sourcepackage.setPackaging(productseries, self.owner)
699+ # Don't forget warty and stable.
700+ all_templates.append(self.factory.makePOTemplate(
701+ productseries=self.stable, name=self.templatename))
702+ all_templates.append(self.factory.makePOTemplate(
703+ distroseries=self.warty, sourcepackagename=self.packagename,
704+ name=self.templatename))
705+ warty_sourcepackage = self.factory.makeSourcePackage(
706+ self.packagename, self.warty)
707+ warty_sourcepackage.setPackaging(self.stable, self.owner)
708+
709+ # Looking from the product side.
710+ subset = self.potemplateset.getSharingSubset(product=self.product)
711+ templates = self._assertStatements(
712+ 1, subset.getSharingPOTemplates(self.templatename))
713+ self.assertContentEqual(all_templates, templates)
714+
715+ # Looking from the sourcepackage side.
716+ subset = self.potemplateset.getSharingSubset(
717+ distribution=self.ubuntu,
718+ sourcepackagename=self.packagename)
719+ templates = self._assertStatements(
720+ 1, subset.getSharingPOTemplates(self.templatename))
721+ self.assertContentEqual(all_templates, templates)
722+
723+ def test_getSharingPOTemplates_product_unrelated_templates(self):
724+ # Sharing templates for a product must not include other templates
725+ # from a linked source package.
726+ self.factory.makePOTemplate(
727+ distroseries=self.hoary, sourcepackagename=self.packagename,
728+ name=self.factory.getUniqueString())
729+ self.factory.makePOTemplate(
730+ distroseries=self.warty, sourcepackagename=self.packagename,
731+ name=self.factory.getUniqueString())
732+ self.factory.makeSourcePackagePublishingHistory(
733+ sourcepackagename=self.packagename,
734+ distroseries=self.hoary)
735+ self.trunk.setPackaging(self.hoary, self.packagename, self.owner)
736+ subset = self.potemplateset.getSharingSubset(product=self.product)
737+ templates = self._assertStatements(
738+ 1, subset.getSharingPOTemplates(self.templatename))
739+
740+ self.assertContentEqual(
741+ [self.trunk_template, self.hoary_template],
742+ templates)
743+
744+ def test_getSharingPOTemplates_product_different_names_and_series(self):
745+ # A product may be packaged into differently named packages in
746+ # different distroseries.
747+ warty_packagename = self.factory.makeSourcePackageName()
748+ warty_template = self.factory.makePOTemplate(
749+ distroseries=self.warty, sourcepackagename=warty_packagename,
750+ name=self.templatename)
751+ self.factory.makeSourcePackagePublishingHistory(
752+ sourcepackagename=self.packagename,
753+ distroseries=self.hoary)
754+ self.trunk.setPackaging(self.hoary, self.packagename, self.owner)
755+ self.factory.makeSourcePackagePublishingHistory(
756+ sourcepackagename=warty_packagename,
757+ distroseries=self.warty)
758+ self.trunk.setPackaging(self.warty, warty_packagename, self.owner)
759+ subset = self.potemplateset.getSharingSubset(product=self.product)
760+ templates = self._assertStatements(
761+ 1, subset.getSharingPOTemplates(self.templatename))
762+
763+ self.assertContentEqual(
764+ [self.trunk_template, self.hoary_template, warty_template],
765+ templates)
766+
767+ def test_getSharingPOTemplates_product_different_names_same_series(self):
768+ # A product may be packaged into differently named packages even in
769+ # the same distroseries. Must use different product series, though.
770+ other_packagename = self.factory.makeSourcePackageName()
771+ other_template = self.factory.makePOTemplate(
772+ distroseries=self.hoary, sourcepackagename=other_packagename,
773+ name=self.templatename)
774+ self.factory.makeSourcePackagePublishingHistory(
775+ sourcepackagename=self.packagename,
776+ distroseries=self.hoary)
777+ self.trunk.setPackaging(self.hoary, self.packagename, self.owner)
778+ self.factory.makeSourcePackagePublishingHistory(
779+ sourcepackagename=other_packagename,
780+ distroseries=self.hoary)
781+ self.stable.setPackaging(self.hoary, other_packagename, self.owner)
782+ subset = self.potemplateset.getSharingSubset(product=self.product)
783+ templates = self._assertStatements(
784+ 1, subset.getSharingPOTemplates(self.templatename))
785+
786+ self.assertContentEqual(
787+ [self.trunk_template, self.hoary_template, other_template],
788+ templates)
789+
790+ def test_getSharingPOTemplates_package_unrelated_template(self):
791+ # Sharing templates for a source package must not include other
792+ # templates from a linked product.
793+ self.factory.makePOTemplate(
794+ productseries=self.trunk, name=self.factory.getUniqueString())
795+ self.factory.makePOTemplate(
796+ productseries=self.stable, name=self.factory.getUniqueString())
797+ sourcepackage = self.factory.makeSourcePackage(
798+ self.packagename, self.hoary)
799+ sourcepackage.setPackaging(self.trunk, self.owner)
800+ subset = self.potemplateset.getSharingSubset(
801+ distribution=self.ubuntu,
802+ sourcepackagename=self.packagename)
803+ templates = self._assertStatements(
804+ 1, subset.getSharingPOTemplates(self.templatename))
805+
806+ self.assertContentEqual(
807+ [self.trunk_template, self.hoary_template],
808+ templates)
809+
810+ def test_getSharingPOTemplates_package_unrelated_template_linked(self):
811+ # Sharing templates for a source package must not include templates
812+ # from sourcepackages of the same name that are linked to a different
813+ # product.
814+ other_productseries = self.factory.makeProductSeries()
815+ other_sourcepackage = self.factory.makeSourcePackage(
816+ self.packagename, self.warty)
817+ other_sourcepackage.setPackaging(other_productseries, self.owner)
818+ other_template = self.factory.makePOTemplate(
819+ productseries=other_productseries, name=self.templatename)
820+
821+ sourcepackage = self.factory.makeSourcePackage(
822+ self.packagename, self.hoary)
823+ sourcepackage.setPackaging(self.trunk, self.owner)
824+ subset = self.potemplateset.getSharingSubset(
825+ distribution=self.ubuntu,
826+ sourcepackagename=self.packagename)
827+ templates = self._assertStatements(
828+ 1, subset.getSharingPOTemplates(self.templatename))
829+
830+ self.assertContentEqual(
831+ [self.trunk_template, self.hoary_template], templates)
832+
833+ # The behavior is controlled by the translation focus of the
834+ # distribution. The series in focus will be selected.
835+ self.ubuntu.translation_focus = self.warty
836+ subset = self.potemplateset.getSharingSubset(
837+ distribution=self.ubuntu,
838+ sourcepackagename=self.packagename)
839+ templates = self._assertStatements(
840+ 1, subset.getSharingPOTemplates(self.templatename))
841+
842+ self.assertContentEqual([other_template], templates)
843+
844+ def test_getSharingPOTemplates_package_only(self):
845+ # Sharing templates for a source package only, is done by the
846+ # sourcepackagename.
847+ warty_template = self.factory.makePOTemplate(
848+ distroseries=self.warty, sourcepackagename=self.packagename,
849+ name=self.templatename)
850+ other_series = self.factory.makeDistroSeries(self.ubuntu)
851+ other_template = self.factory.makePOTemplate(
852+ distroseries=other_series, sourcepackagename=self.packagename,
853+ name=self.templatename)
854+ subset = self.potemplateset.getSharingSubset(
855+ distribution=self.ubuntu,
856+ sourcepackagename=self.packagename)
857+ templates = self._assertStatements(
858+ 1, subset.getSharingPOTemplates(self.templatename))
859+
860+ self.assertContentEqual(
861+ [self.hoary_template, other_template, warty_template], templates)
862+
863+ def test_getSharingPOTemplates_package_one_linked(self):
864+ # Once one a sourcepackage in a distroseries that is neither the
865+ # translation focus nor the current series is linked to a product,
866+ # no sharing by name is possible anymore.
867+ self.factory.makePOTemplate(
868+ distroseries=self.warty, sourcepackagename=self.packagename,
869+ name=self.templatename)
870+ other_series = self.factory.makeDistroSeries(self.ubuntu)
871+ self.factory.makePOTemplate(
872+ distroseries=other_series, sourcepackagename=self.packagename,
873+ name=self.templatename)
874+ other_sourcepackage = self.factory.makeSourcePackage(
875+ self.packagename, other_series)
876+ other_sourcepackage.setPackaging(self.trunk, self.owner)
877+
878+ subset = self.potemplateset.getSharingSubset(
879+ distribution=self.ubuntu,
880+ sourcepackagename=self.packagename)
881+ templates = self._assertStatements(
882+ 0, subset.getSharingPOTemplates(self.templatename))
883+
884+ self.assertEqual([], templates)
885+
886+ def test_getOrCreateSharedPOTMsgSet_product(self):
887+ # Trying to create an identical POTMsgSet in a product as exists
888+ # in a linked sourcepackage will return the existing POTMsgset.
889+ self.factory.makeSourcePackagePublishingHistory(
890+ sourcepackagename=self.packagename,
891+ distroseries=self.hoary)
892+ self.trunk.setPackaging(self.hoary, self.packagename, self.owner)
893+ hoary_potmsgset = self.factory.makePOTMsgSet(
894+ potemplate=self.hoary_template, sequence=1)
895+
896+ trunk_potmsgset = self.trunk_template.getOrCreateSharedPOTMsgSet(
897+ singular_text=hoary_potmsgset.singular_text,
898+ plural_text=hoary_potmsgset.plural_text)
899+ self.assertEqual(hoary_potmsgset, trunk_potmsgset)
900+
901+ def test_getOrCreateSharedPOTMsgSet_package(self):
902+ # Trying to create an identical POTMsgSet in a product as exists
903+ # in a linked sourcepackage will return the existing POTMsgset.
904+ self.factory.makeSourcePackagePublishingHistory(
905+ sourcepackagename=self.packagename,
906+ distroseries=self.hoary)
907+ self.trunk.setPackaging(self.hoary, self.packagename, self.owner)
908+ hoary_potmsgset = self.factory.makePOTMsgSet(
909+ potemplate=self.hoary_template, sequence=1)
910+
911+ trunk_potmsgset = self.trunk_template.getOrCreateSharedPOTMsgSet(
912+ singular_text=hoary_potmsgset.singular_text,
913+ plural_text=hoary_potmsgset.plural_text)
914+ self.assertEqual(hoary_potmsgset, trunk_potmsgset)
915+
916+
917 def test_suite():
918 return unittest.TestLoader().loadTestsFromName(__name__)

Subscribers

People subscribed via source and target branches