Merge lp:~jtv/launchpad/recife-552639 into lp:~launchpad/launchpad/recife
- recife-552639
- Merge into recife
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Jeroen T. Vermeulen | ||||
Approved revision: | no longer in the source branch. | ||||
Merged at revision: | 9140 | ||||
Proposed branch: | lp:~jtv/launchpad/recife-552639 | ||||
Merge into: | lp:~launchpad/launchpad/recife | ||||
Diff against target: |
752 lines (+509/-30) 8 files modified
database/schema/security.cfg (+1/-1) lib/lp/translations/interfaces/potmsgset.py (+21/-0) lib/lp/translations/interfaces/translationmessage.py (+3/-0) lib/lp/translations/interfaces/translations.py (+12/-3) lib/lp/translations/model/potmsgset.py (+308/-24) lib/lp/translations/model/translationmessage.py (+5/-0) lib/lp/translations/tests/test_potmsgset.py (+122/-1) lib/lp/translations/tests/test_translationmessage.py (+37/-1) |
||||
To merge this branch: | bzr merge lp:~jtv/launchpad/recife-552639 | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Henning Eggers (community) | code | Approve | |
Review via email: mp+24883@code.launchpad.net |
Commit message
Implement setCurrentTrans
Description of the change
= Bug 552639 =
This implements the unconditional setting of a translation for a particular message as current in Ubuntu, or upstream, or both. See the spec: https:/
Jeroen T. Vermeulen (jtv) wrote : | # |
Jeroen T. Vermeulen (jtv) wrote : | # |
The tests in danilo's separate branch now pass. I've made a few design changes, fixed one or two mistakes in the code, and changed the expected behavior of a few tests. But for something this big, not bad.
The few relevant tests that are in this branch you can run with:
{{{
./bin/test -vv -t test_potmsgset
}}}
If you're not using the feature branch (lp:~launchpad/launchpad/recife) as a merge base you will see some failures from resetCurrentTra
I'm going to put up the test suite for separate review, in order to keep line count in check.
Henning Eggers (henninge) wrote : | # |
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1
> === modified file 'database/
OK
> === modified file 'lib/lp/
> --- lib/lp/
> +++ lib/lp/
> @@ -214,6 +214,10 @@
> force_edition_
> """Update or create a translation message using `new_translations`.
>
> + This method is Launchpad Translations's sliderule: it does
> + everything, nobody fully understands it all, and we intend to
> + replace it with a range of less flexible tools.
> +
> :param pofile: a `POFile` to add `new_translations` to.
> :param submitter: author of the translations.
> :param new_translations: a dictionary of plural forms, with the
> @@ -261,6 +265,20 @@
> If a translation conflict is detected, TranslationConflict is raised.
> """
>
> + def setCurrentTrans
> + translation_side, share_with_
> + """Set the message's translation in Ubuntu, or upstream, or both.
> +
> + :param pofile:
> + :param submitter:
> + :param translations:
> + :param origin:
> + :param translation_side: a `TranslationSide` value.
> + :param share_with_
> + """
> + # XXX: Check signature before completing branch.
> + # XXX: Update docstring.
So, these XXX are supposed to be gone before we land the feature, right?
> +
> def resetCurrentTra
> """Reset the currently used translation.
>
> === modified file 'lib/lp/
OK
> === modified file 'lib/lp/
> --- lib/lp/
> +++ lib/lp/
> @@ -4,11 +4,16 @@
> # pylint: disable-
>
> __metaclass__ = type
> -__all__ = ['POTMsgSet']
> +__all__ = [
> + 'make_translati
> + 'POTMsgSet',
> + ]
> +
>
> import datetime
> import logging
> import pytz
> +import re
>
> from zope.interface import implements
> from zope.component import getUtility
> @@ -41,7 +46,8 @@
> RosettaTranslat
> TranslationConf
> TranslationVali
> -from lp.translations
> +from lp.translations
> + TranslationCons
> from lp.translations
> from lp.translations
> from lp.translations
> @@ -79,6 +85,91 @@
> u'credits are counted as translated.')
>
>
> +class TranslationSide
> + """Dealing with a `POTMsgSet` on either `TranslationSide`.
> +
> + Encapsulates primitives that depend on translation side: finding the
> + message that ...
Jeroen T. Vermeulen (jtv) wrote : | # |
> > === modified file 'lib/lp/
> > --- lib/lp/
> +0000
> > +++ lib/lp/
> +0000
> > @@ -214,6 +214,10 @@
> > force_edition_
> > """Update or create a translation message using `new_translations`.
> >
> > + This method is Launchpad Translations's sliderule: it does
> > + everything, nobody fully understands it all, and we intend to
> > + replace it with a range of less flexible tools.
> > +
> > :param pofile: a `POFile` to add `new_translations` to.
> > :param submitter: author of the translations.
> > :param new_translations: a dictionary of plural forms, with the
> > @@ -261,6 +265,20 @@
> > If a translation conflict is detected, TranslationConflict is
> raised.
> > """
> >
> > + def setCurrentTrans
> > + translation_side,
> share_with_
> > + """Set the message's translation in Ubuntu, or upstream, or both.
> > +
> > + :param pofile:
> > + :param submitter:
> > + :param translations:
> > + :param origin:
> > + :param translation_side: a `TranslationSide` value.
> > + :param share_with_
> > + """
> > + # XXX: Check signature before completing branch.
> > + # XXX: Update docstring.
>
> So, these XXX are supposed to be gone before we land the feature, right?
Yes, I've fixed them now.
Once a long time ago I proposed allowing "reviewer notes" in the code. That was turned down, but now I sometimes include these malformed XXX comments to ensure that the reviewer will say something if I let them through.
> > === modified file 'lib/lp/
> > --- lib/lp/
> > +++ lib/lp/
> > @@ -4,11 +4,16 @@
> > # pylint: disable-
> >
> > __metaclass__ = type
> > -__all__ = ['POTMsgSet']
> > +__all__ = [
> > + 'make_translati
> > + 'POTMsgSet',
> > + ]
> > +
> >
> > import datetime
> > import logging
> > import pytz
> > +import re
> >
> > from zope.interface import implements
> > from zope.component import getUtility
> > @@ -41,7 +46,8 @@
> > RosettaTranslat
> > TranslationConf
> > TranslationVali
> > -from lp.translations
> > +from lp.translations
> > + TranslationCons
> > from lp.translations
> > from lp.translations
> > from lp.translations
> > @@ -79,6 +85,91 @@
> > u'credits are counted as translated.')
> >
> >
> > +class TranslationSide
> > + """Dealing with a `POTMsgSet` on either `Translatio...
Jeroen T. Vermeulen (jtv) wrote : | # |
No idea how I managed to post my unfinished reply by accident just now, but I'll just continue where I left off.
> > + elif character == 'Z':
> > + # There is no incumbent message, so do nothing to it.
> > + assert incumbent_message is None, (
> > + "Incorrect Z in decision matrix.")
>
> Ok, first you hide this check in _nameMessageStatus, now you pull it out in
> the open for an assert. Maybe you should trust your matrix more and just put a
> "pass" in here?
Again rather not in this phase, where we want to be sure we can tweak things without breaking them. Once we start refactoring, the Z probably becomes a no-op anyway.
> > + elif character == '1':
> > + # Create & activate.
> > + message = self._makeTrans
> > + pofile, submitter, translations, origin)
> > + elif character == '2':
> > + # Create, diverge, activate.
> > + message = self._makeTrans
> > + pofile, submitter, translations, origin, diverged=True)
> > + elif character == '4':
> > + # Activate.
> > + message = twin
> > + elif character == '5':
> > + # If other is a suggestion, diverge and activate.
> > + # (If not, it's already active and has been unmasked by
> > + # our deactivating the incumbent).
> > + message = twin
> > + if not traits.
> > + assert not traits.
> > + "Decision matrix says '5' for a message that's "
> > + "active on the other side.")
>
> Yes, that is what is done in _nameMessageStatus, so duplicated here. Also, the
> error message is quite cryptic to an outsider.
Alright, I've made the message clearer.
> > +
> > + traits.
> > +
> > + return message
> > +
> > +>>>>>>> MERGE-SOURCE
> > def resetCurrentTra
> > """See `IPOTMsgSet`."""
> >
> > @@ -1145,7 +1423,7 @@
> > # The credits message has a fixed "translator."
> > translator = getUtility(
> >
> > - message = self.updateTran
> > + self.updateTran
> > pofile, translator, [credits_
> > is_current_
> > force_shared=True, force_edition_
> > === modified file 'lib/lp/
>
> The tests look very good although I am sure there could be even more.... ;-)
I'm not sure what you mean. As you know (and as mentioned several times in the initial comments for this MP) there is a separate branch with exhaustive tests.
Jeroen
Henning Eggers (henninge) wrote : | # |
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1
Am 19.06.2010 07:27, schrieb Jeroen T. Vermeulen:
[...]
>>> +=======
>>> + def _nameMessageSta
>>> + """Figure out the decision-matrix status of a message.
>>> +
>>> + This is used in navigating the decision matrix in
>>> + `setCurrentTran
>>> + """
>>> + if message is None:
>>> + return 'none'
>>> + elif message.potemplate is None:
>>> + if translation_
>>> + return 'other_shared'
>>> + else:
>>> + return 'shared'
>>> + else:
>>> + assert message.potemplate is not None, "Confused message
>> state."
>>
>> Your either forgot this in here or it's paranoia. Since the preceding elif
>> checks the exact same condition, this really is not needed.
>
> It's also partly caused by the "there must be an else" rule. I resolved this by:
Well, the usual way to fulfill that is "comment and pass". But the new 'if'
statement is much better anyway.
>
> 1. Adding an ITranslationMes
> 2. Moving the "else" clause above the "elif" clause.
> 3. Flattening the "if" inside the "elif" clause to be part of the main if/elif/else.
>
> Looks much nicer now. We should have added is_diverged earlier.
Yes, definitely! Thanks for finally doing this.
>
>
>>> + def setCurrentTrans
>> origin,
>>> + translation_side,
>> share_with_
>>> + """See `IPOTMsgSet`."""
>>> + traits = make_translatio
>>> + translation_side, self, pofile.potemplate, pofile.language,
>>> + variant=
>>> +
>>> + translations = self._findPOTra
>>> + incumbent_message = traits.
>>> + twin = self._findTrans
>>> + pofile, translations, prefer_
>>
>> Hm, I am not 100% sure what a twin is. Maybe you could explain that here?
>
> I thought the blueprint made that clear, but I've now documented it in the code.
Oh, I guess I was too slow. But "twin" just never appeared before. Thanks for
documenting it now.
>
>
>>> + # Summary of the matrix:
>>> + # * If the incumbent message is diverged and we're setting a
>>> + # translation that's already shared: converge.
>>> + # * If the incumbent message is diverged and we're setting a
>>> + # translation that's not already shared: maintain divergence.
>>> + # * If the incumbent message is shared, replace it.
>>> + # * If there is no twin, simply create a new message (shared or
>>> + # diverged depending; see above).
>>> + # * If there is a shared twin, activate it (but also diverge if
>>> + # necessary; see above).
>>> + # * If there is a diverged twin, activate it (and converge it
>>> + # if appropriate; see above).
>>> + # * If there is a twin that's shared on the other side,
>>> +
>...
Henning Eggers (henninge) wrote : | # |
Here one comment, I forgot.
> === modified file 'lib/lp/
> --- lib/lp/
> +++ lib/lp/
> @@ -1,4 +1,4 @@
> -# Copyright 2009 Canonical Ltd. This software is licensed under the
> +# Copyright 2009-2010 Canonical Ltd. This software is licensed under the
> # GNU Affero General Public License version 3 (see the file LICENSE).
>
> """Unit tests for `TranslationMes
> @@ -10,12 +10,46 @@
> from zope.component import getUtility
> from zope.security.proxy import removeSecurityProxy
>
> +from canonical.
> from lp.services.
> from lp.testing import TestCaseWithFactory
> from lp.translations
> +from lp.translations
> +from lp.translations
> + ITranslationMes
> from lp.translations
> from canonical.testing import ZopelessDatabas
>
> +
> +class TestTranslation
> + """Unit tests for `TranslationMes
> +
> + There aren't many of these. We didn't do much unit testing back
> + then.
> + """
And thanks a lot for adding these! ;-)
> +
> + layer = ZopelessDatabas
> +
> + def test_baseline(
> + message = self.factory.
> + verifyObject(
I'd have split this test here.
> +
> + pofile = self.factory.
> + potmsgset = self.factory.
> + dummy = DummyTranslatio
> + verifyObject(
> +
> + def test_is_
> + # ITranslationMes
> + # say "message.
> + # "message.potemplate is not None."
> + message = self.factory.
> + self.assertFals
> +
And this one here.
> + message = self.factory.
> + self.assertTrue
But that is just how I would do that. Makes test failures easier to spot.
Also, unrelated parts of a test will not be run if one fails. Maybe that
second reasoning is even stronger.
> +
> +
> class TestTranslation
> """Tests for `TranslationMes
>
Cheers, Henning
Jeroen T. Vermeulen (jtv) wrote : | # |
Hi Henning,
Thanks for the review. You were right: my change to that assertion text was lost somehow. Good thing you spotted that. Hope it's fixed this time.
> > === modified file 'lib/lp/
> > --- lib/lp/
> 20:15:38 +0000
> > +++ lib/lp/
> 03:38:37 +0000
> > """Unit tests for `TranslationMes
> > + def test_baseline(
> > + message = self.factory.
> > + verifyObject(
>
> I'd have split this test here.
Good idea. I've been letting them grow longer so they'd waste less time on setup and teardown, sort of like our old doctests, but it's not always a good thing.
> > + def test_is_
> > + # ITranslationMes
> > + # say "message.
> > + # "message.potemplate is not None."
> > + message = self.factory.
> > + self.assertFals
> > +
>
> And this one here.
Both split now.
Jeroen
Preview Diff
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-21 16:42:29 +0000 |
4 | @@ -258,7 +258,7 @@ |
5 | public.temporaryblobstorage = SELECT, INSERT, DELETE |
6 | public.translationgroup = SELECT, INSERT, UPDATE |
7 | public.translationimportqueueentry = SELECT, INSERT, UPDATE, DELETE |
8 | -public.translationmessage = SELECT, INSERT, UPDATE |
9 | +public.translationmessage = SELECT, INSERT, UPDATE, DELETE |
10 | public.translator = SELECT, INSERT, UPDATE, DELETE |
11 | public.usertouseremail = SELECT, UPDATE |
12 | public.validpersoncache = SELECT |
13 | |
14 | === modified file 'lib/lp/translations/interfaces/potmsgset.py' |
15 | --- lib/lp/translations/interfaces/potmsgset.py 2010-06-09 14:17:32 +0000 |
16 | +++ lib/lp/translations/interfaces/potmsgset.py 2010-06-21 16:42:29 +0000 |
17 | @@ -214,6 +214,10 @@ |
18 | force_edition_rights=False, allow_credits=False): |
19 | """Update or create a translation message using `new_translations`. |
20 | |
21 | + This method is Launchpad Translations's sliderule: it does |
22 | + everything, nobody fully understands it all, and we intend to |
23 | + replace it with a range of less flexible tools. |
24 | + |
25 | :param pofile: a `POFile` to add `new_translations` to. |
26 | :param submitter: author of the translations. |
27 | :param new_translations: a dictionary of plural forms, with the |
28 | @@ -261,6 +265,23 @@ |
29 | If a translation conflict is detected, TranslationConflict is raised. |
30 | """ |
31 | |
32 | + def setCurrentTranslation(pofile, submitter, translations, origin, |
33 | + translation_side, share_with_other_side=False): |
34 | + """Set the message's translation in Ubuntu, or upstream, or both. |
35 | + |
36 | + :param pofile: `POFile` you're setting translations in. Other |
37 | + `POFiles` that share translations with this one may also be |
38 | + affected. |
39 | + :param submitter: `Person` who is setting these translations. |
40 | + :param translations: a dict mapping plural-form numbers to the |
41 | + translated string for that form. |
42 | + :param origin: A `RosettaTranslationOrigin`. |
43 | + :param translation_side: The `TranslationSide` that this |
44 | + translation is for (Ubuntu or upstream). |
45 | + :param share_with_other_side: When sharing this translation, |
46 | + share it with the other `TranslationSide` as well. |
47 | + """ |
48 | + |
49 | def resetCurrentTranslation(pofile, lock_timestamp): |
50 | """Reset the currently used translation. |
51 | |
52 | |
53 | === modified file 'lib/lp/translations/interfaces/translationmessage.py' |
54 | --- lib/lp/translations/interfaces/translationmessage.py 2010-03-26 19:49:46 +0000 |
55 | +++ lib/lp/translations/interfaces/translationmessage.py 2010-06-21 16:42:29 +0000 |
56 | @@ -209,6 +209,9 @@ |
57 | title=_("Number of plural form translations in this translation."), |
58 | readonly=True, required=True) |
59 | |
60 | + is_diverged = Bool( |
61 | + title=_("Is this message diverged?"), readonly=True, required=True) |
62 | + |
63 | def setPOFile(pofile): |
64 | """Set a POFile for use in views.""" |
65 | |
66 | |
67 | === modified file 'lib/lp/translations/interfaces/translations.py' |
68 | --- lib/lp/translations/interfaces/translations.py 2009-07-17 00:26:05 +0000 |
69 | +++ lib/lp/translations/interfaces/translations.py 2010-06-21 16:42:29 +0000 |
70 | @@ -1,4 +1,4 @@ |
71 | -# Copyright 2009 Canonical Ltd. This software is licensed under the |
72 | +# Copyright 2009-2010 Canonical Ltd. This software is licensed under the |
73 | # GNU Affero General Public License version 3 (see the file LICENSE). |
74 | |
75 | # pylint: disable-msg=E0211,E0213 |
76 | @@ -7,10 +7,12 @@ |
77 | |
78 | __metaclass__ = type |
79 | |
80 | -__all__ = ( |
81 | +__all__ = [ |
82 | 'TranslationConstants', |
83 | 'TranslationsBranchImportMode', |
84 | - ) |
85 | + 'TranslationSide', |
86 | + ] |
87 | + |
88 | |
89 | class TranslationConstants: |
90 | """Set of constants used inside the context of translations.""" |
91 | @@ -52,3 +54,10 @@ |
92 | """) |
93 | |
94 | |
95 | +class TranslationSide: |
96 | + """The two "sides" of software that can be translated in Launchpad. |
97 | + |
98 | + These are "upstream" and "Ubuntu." |
99 | + """ |
100 | + UPSTREAM = 1 |
101 | + UBUNTU = 2 |
102 | |
103 | === modified file 'lib/lp/translations/model/potmsgset.py' |
104 | --- lib/lp/translations/model/potmsgset.py 2010-06-10 11:24:51 +0000 |
105 | +++ lib/lp/translations/model/potmsgset.py 2010-06-21 16:42:29 +0000 |
106 | @@ -4,11 +4,16 @@ |
107 | # pylint: disable-msg=E0611,W0212 |
108 | |
109 | __metaclass__ = type |
110 | -__all__ = ['POTMsgSet'] |
111 | +__all__ = [ |
112 | + 'make_translation_side_message_traits', |
113 | + 'POTMsgSet', |
114 | + ] |
115 | + |
116 | |
117 | import datetime |
118 | import logging |
119 | import pytz |
120 | +import re |
121 | |
122 | from zope.interface import implements |
123 | from zope.component import getUtility |
124 | @@ -41,7 +46,8 @@ |
125 | RosettaTranslationOrigin, |
126 | TranslationConflict, |
127 | TranslationValidationStatus) |
128 | -from lp.translations.interfaces.translations import TranslationConstants |
129 | +from lp.translations.interfaces.translations import ( |
130 | + TranslationConstants, TranslationSide) |
131 | from lp.translations.model.pomsgid import POMsgID |
132 | from lp.translations.model.potranslation import POTranslation |
133 | from lp.translations.model.translationmessage import ( |
134 | @@ -79,6 +85,100 @@ |
135 | u'credits are counted as translated.') |
136 | |
137 | |
138 | +class TranslationSideMessageTraits: |
139 | + """Dealing with a `POTMsgSet` on either `TranslationSide`. |
140 | + |
141 | + Encapsulates primitives that depend on translation side: finding the |
142 | + message that is current on the given side, checking the flag that |
143 | + says whether a message is current on this side, setting or clearing |
144 | + the flag, and providing the same capabilities for the other side. |
145 | + |
146 | + For an introduction to the Traits pattern, see |
147 | + http://www.cantrip.org/traits.html |
148 | + """ |
149 | + # The TranslationSide that these Traits are for. |
150 | + side = None |
151 | + |
152 | + # TranslationSideMessageTraits for this message on the "other side." |
153 | + other_side = None |
154 | + |
155 | + # Name of this side's flag. |
156 | + flag_name = None |
157 | + |
158 | + def __init__(self, potmsgset, potemplate=None, language=None, |
159 | + variant=None): |
160 | + self.potmsgset = potmsgset |
161 | + self.potemplate = potemplate |
162 | + self.language = language |
163 | + self.variant = variant |
164 | + |
165 | + self._found_incumbent = False |
166 | + |
167 | + @property |
168 | + def incumbent_message(self): |
169 | + """Message that currently has the flag.""" |
170 | + if not self._found_incumbent: |
171 | + self._incumbent = self._getIncumbentMessage() |
172 | + self._found_incumbent = True |
173 | + return self._incumbent |
174 | + |
175 | + def getFlag(self, translationmessage): |
176 | + """Is this message the current one on this side?""" |
177 | + return getattr(translationmessage, self.flag_name) |
178 | + |
179 | + def setFlag(self, translationmessage, value): |
180 | + """Set or clear a message's "current" flag for this side.""" |
181 | + if value == self.getFlag(translationmessage): |
182 | + return |
183 | + |
184 | + if value and self.incumbent_message is not None: |
185 | + self.setFlag(self.incumbent_message, False) |
186 | + |
187 | + setattr(translationmessage, self.flag_name, value) |
188 | + self._found_incumbent = False |
189 | + |
190 | + def _getIncumbentMessage(self): |
191 | + """Get the message that is current on this side, if any.""" |
192 | + raise NotImplementedError('_getIncumbentMessage') |
193 | + |
194 | + |
195 | +class UpstreamSideTraits(TranslationSideMessageTraits): |
196 | + """Traits for upstream translations.""" |
197 | + |
198 | + side = TranslationSide.UPSTREAM |
199 | + |
200 | + flag_name = 'is_current_upstream' |
201 | + |
202 | + def _getIncumbentMessage(self): |
203 | + """See `TranslationSideMessageTraits`.""" |
204 | + return self.potmsgset.getImportedTranslationMessage( |
205 | + self.potemplate, self.language, variant=self.variant) |
206 | + |
207 | + |
208 | +class UbuntuSideTraits(TranslationSideMessageTraits): |
209 | + """Traits for Ubuntu translations.""" |
210 | + |
211 | + side = TranslationSide.UBUNTU |
212 | + |
213 | + flag_name = 'is_current_ubuntu' |
214 | + |
215 | + def _getIncumbentMessage(self): |
216 | + """See `TranslationSideMessageTraits`.""" |
217 | + return self.potmsgset.getCurrentTranslationMessage( |
218 | + self.potemplate, self.language, variant=self.variant) |
219 | + |
220 | + |
221 | +def make_translation_side_message_traits(side, potmsgset, potemplate, |
222 | + language, variant=None): |
223 | + """Create `TranslationSideTraits` object of the appropriate subtype.""" |
224 | + ubuntu = UbuntuSideTraits(potmsgset, potemplate, language, variant) |
225 | + upstream = UpstreamSideTraits(potmsgset, potemplate, language, variant) |
226 | + upstream.other_side = ubuntu |
227 | + ubuntu.other_side = upstream |
228 | + mapping = dict((traits.side, traits) for traits in (ubuntu, upstream)) |
229 | + return mapping[side] |
230 | + |
231 | + |
232 | class POTMsgSet(SQLBase): |
233 | implements(IPOTMsgSet) |
234 | |
235 | @@ -232,13 +332,13 @@ |
236 | current=True): |
237 | """Get a translation message which is either used in |
238 | Launchpad (current=True) or in an import (current=False). |
239 | - |
240 | + |
241 | Prefers a diverged message if present. |
242 | """ |
243 | - # Change the is_current_ubuntu and is_current_upstream conditions |
244 | - # with extreme care: they need to match condition specified in |
245 | - # indexes, or postgres may not pick them up (in complicated queries, |
246 | - # postgres query optimizer sometimes does text-matching of indexes). |
247 | + # Change 'is_current IS TRUE' and 'is_imported IS TRUE' conditions |
248 | + # carefully: they need to match condition specified in indexes, |
249 | + # or Postgres may not pick them up (in complicated queries, |
250 | + # Postgres query optimizer sometimes does text-matching of indexes). |
251 | if current: |
252 | used_clause = 'is_current_ubuntu IS TRUE' |
253 | else: |
254 | @@ -522,13 +622,17 @@ |
255 | potranslations[pluralform] = None |
256 | return potranslations |
257 | |
258 | - def _findTranslationMessage(self, pofile, potranslations, pluralforms): |
259 | - """Find a message for this `pofile`. |
260 | - |
261 | - The returned message matches exactly the given `translations` strings |
262 | - comparing only `pluralforms` of them. |
263 | + def _findTranslationMessage(self, pofile, potranslations, |
264 | + prefer_shared=True): |
265 | + """Find a matching message in this `pofile`. |
266 | + |
267 | + The returned message matches exactly the given `translations` |
268 | + strings (except plural forms not supported by `pofile`, which |
269 | + are ignored). |
270 | + |
271 | :param potranslations: A list of translation strings. |
272 | - :param pluralforms: The number of pluralforms to compare. |
273 | + :param prefer_shared: Whether to prefer a shared match over a |
274 | + diverged one. |
275 | """ |
276 | clauses = ['potmsgset = %s' % sqlvalues(self), |
277 | 'language = %s' % sqlvalues(pofile.language), |
278 | @@ -539,7 +643,7 @@ |
279 | else: |
280 | clauses.append('variant = %s' % sqlvalues(pofile.variant)) |
281 | |
282 | - for pluralform in range(pluralforms): |
283 | + for pluralform in range(pofile.plural_forms): |
284 | if potranslations[pluralform] is None: |
285 | clauses.append('msgstr%s IS NULL' % sqlvalues(pluralform)) |
286 | else: |
287 | @@ -547,10 +651,15 @@ |
288 | sqlvalues(pluralform, potranslations[pluralform]))) |
289 | |
290 | remaining_plural_forms = range( |
291 | - pluralforms, TranslationConstants.MAX_PLURAL_FORMS) |
292 | - |
293 | - # Prefer shared messages over diverged ones. |
294 | - order = ['potemplate NULLS FIRST'] |
295 | + pofile.plural_forms, TranslationConstants.MAX_PLURAL_FORMS) |
296 | + |
297 | + # Prefer either shared or diverged messages, depending on |
298 | + # arguments. |
299 | + if prefer_shared: |
300 | + order = ['potemplate NULLS FIRST'] |
301 | + else: |
302 | + order = ['potemplate NULLS LAST'] |
303 | + |
304 | # Normally at most one message should match. But if there is |
305 | # more than one, prefer the one that adds the fewest extraneous |
306 | # plural forms. |
307 | @@ -564,9 +673,9 @@ |
308 | if len(matches) > 0: |
309 | if len(matches) > 1: |
310 | logging.warn( |
311 | - "Translation for POTMsgSet %s into %s " |
312 | - "matches %s existing translations." % sqlvalues( |
313 | - self, pofile.language.code, len(matches))) |
314 | + "Translation for POTMsgSet %s into '%s' " |
315 | + "matches %s existing translations.", |
316 | + self.id, pofile.language.code, len(matches)) |
317 | return matches[0] |
318 | else: |
319 | return None |
320 | @@ -799,7 +908,7 @@ |
321 | # of translations. None if there is no such message and needs to be |
322 | # created. |
323 | matching_message = self._findTranslationMessage( |
324 | - pofile, potranslations, pofile.plural_forms) |
325 | + pofile, potranslations) |
326 | |
327 | match_is_upstream = ( |
328 | matching_message is not None and |
329 | @@ -897,7 +1006,7 @@ |
330 | potranslations = self._findPOTranslations(new_translations) |
331 | |
332 | existing_message = self._findTranslationMessage( |
333 | - pofile, potranslations, pofile.plural_forms) |
334 | + pofile, potranslations) |
335 | if existing_message is not None: |
336 | return existing_message |
337 | |
338 | @@ -951,9 +1060,184 @@ |
339 | current.reviewer = reviewer |
340 | current.date_reviewed = lock_timestamp |
341 | |
342 | + def _nameMessageStatus(self, message, translation_side_traits): |
343 | + """Figure out the decision-matrix status of a message. |
344 | + |
345 | + This is used in navigating the decision matrix in |
346 | + `setCurrentTranslation`. |
347 | + """ |
348 | + if message is None: |
349 | + return 'none' |
350 | + elif message.is_diverged: |
351 | + return 'diverged' |
352 | + elif translation_side_traits.other_side.getFlag(message): |
353 | + return 'other_shared' |
354 | + else: |
355 | + return 'shared' |
356 | + |
357 | + def _makeTranslationMessage(self, pofile, submitter, translations, origin, |
358 | + diverged=False): |
359 | + """Create a new `TranslationMessage`. |
360 | + |
361 | + The message will not be made current on either side (Ubuntu or |
362 | + upstream), but it can be diverged. Only messages that are |
363 | + current should be diverged, but it's up to the caller to ensure |
364 | + the right state. |
365 | + """ |
366 | + if diverged: |
367 | + potemplate = pofile.potemplate |
368 | + else: |
369 | + potemplate = None |
370 | + |
371 | + translation_args = dict( |
372 | + ('msgstr%d' % form, translation) |
373 | + for form, translation in translations.iteritems() |
374 | + ) |
375 | + |
376 | + return TranslationMessage( |
377 | + potmsgset=self, |
378 | + potemplate=potemplate, |
379 | + pofile=pofile, |
380 | + language=pofile.language, |
381 | + variant=pofile.variant, |
382 | + origin=origin, |
383 | + submitter=submitter, |
384 | + validation_status=TranslationValidationStatus.OK, |
385 | + **translation_args) |
386 | + |
387 | def setCurrentTranslation(self, pofile, submitter, translations, origin, |
388 | translation_side, share_with_other_side=False): |
389 | """See `IPOTMsgSet`.""" |
390 | + traits = make_translation_side_message_traits( |
391 | + translation_side, self, pofile.potemplate, pofile.language, |
392 | + variant=pofile.variant) |
393 | + |
394 | + translations = self._findPOTranslations(translations) |
395 | + |
396 | + # The current message on this translation side, if any. |
397 | + incumbent_message = traits.incumbent_message |
398 | + |
399 | + # An already existing message, if any, that's either shared, or |
400 | + # diverged for the template/pofile we're working on, whose |
401 | + # translations are identical to the ones we're setting. |
402 | + twin = self._findTranslationMessage( |
403 | + pofile, translations, prefer_shared=False) |
404 | + |
405 | + # Summary of the matrix: |
406 | + # * If the incumbent message is diverged and we're setting a |
407 | + # translation that's already shared: converge. |
408 | + # * If the incumbent message is diverged and we're setting a |
409 | + # translation that's not already shared: maintain divergence. |
410 | + # * If the incumbent message is shared, replace it. |
411 | + # * If there is no twin, simply create a new message (shared or |
412 | + # diverged depending; see above). |
413 | + # * If there is a shared twin, activate it (but also diverge if |
414 | + # necessary; see above). |
415 | + # * If there is a diverged twin, activate it (and converge it |
416 | + # if appropriate; see above). |
417 | + # * If there is a twin that's shared on the other side, |
418 | + |
419 | + decision_matrix = { |
420 | + 'incumbent_none': { |
421 | + 'twin_none': 'Z1*', |
422 | + 'twin_shared': 'Z4*', |
423 | + 'twin_diverged': 'Z7*', |
424 | + 'twin_other_shared': 'Z4', |
425 | + }, |
426 | + 'incumbent_shared': { |
427 | + 'twin_none': 'B1*', |
428 | + 'twin_shared': 'B4*', |
429 | + 'twin_diverged': 'B7*', |
430 | + 'twin_other_shared': 'B4', |
431 | + }, |
432 | + 'incumbent_diverged': { |
433 | + 'twin_none': 'A2', |
434 | + 'twin_shared': 'A5', |
435 | + 'twin_diverged': 'A4', |
436 | + 'twin_other_shared': 'A6', |
437 | + }, |
438 | + 'incumbent_other_shared': { |
439 | + 'twin_none': 'B1+', |
440 | + 'twin_shared': 'B4+', |
441 | + 'twin_diverged': 'B7+', |
442 | + 'twin_other_shared': '', |
443 | + }, |
444 | + } |
445 | + |
446 | + incumbent_state = "incumbent_%s" % self._nameMessageStatus( |
447 | + incumbent_message, traits) |
448 | + twin_state = "twin_%s" % self._nameMessageStatus(twin, traits) |
449 | + |
450 | + decisions = decision_matrix[incumbent_state][twin_state] |
451 | + assert re.match('[ABZ]?[124567]?[+*]?$', decisions), ( |
452 | + "Bad decision string.") |
453 | + |
454 | + for character in decisions: |
455 | + if character == 'A': |
456 | + # Deactivate & converge. |
457 | + # There may be an identical shared message. |
458 | + traits.setFlag(incumbent_message, False) |
459 | + incumbent_message.shareIfPossible() |
460 | + elif character == 'B': |
461 | + # Deactivate. |
462 | + traits.setFlag(incumbent_message, False) |
463 | + elif character == 'Z': |
464 | + # There is no incumbent message, so do nothing to it. |
465 | + assert incumbent_message is None, ( |
466 | + "Incorrect Z in decision matrix.") |
467 | + elif character == '1': |
468 | + # Create & activate. |
469 | + message = self._makeTranslationMessage( |
470 | + pofile, submitter, translations, origin) |
471 | + elif character == '2': |
472 | + # Create, diverge, activate. |
473 | + message = self._makeTranslationMessage( |
474 | + pofile, submitter, translations, origin, diverged=True) |
475 | + elif character == '4': |
476 | + # Activate. |
477 | + message = twin |
478 | + elif character == '5': |
479 | + # If other is a suggestion, diverge and activate. |
480 | + # (If not, it's already active and has been unmasked by |
481 | + # our deactivating the incumbent). |
482 | + message = twin |
483 | + if not traits.getFlag(twin): |
484 | + assert not traits.other_side.getFlag(twin), ( |
485 | + "Trying to diverge a message that is current on the " |
486 | + "other side.") |
487 | + message.potemplate = pofile.potemplate |
488 | + elif character == '6': |
489 | + # If other is not active, fork a diverged message. |
490 | + if traits.getFlag(twin): |
491 | + message = twin |
492 | + else: |
493 | + # The twin is used on the other side, so we can't |
494 | + # just reuse it for our diverged message. Create a |
495 | + # new one. |
496 | + message = self._makeTranslationMessage( |
497 | + pofile, submitter, translations, origin, |
498 | + diverged=True) |
499 | + elif character == '7': |
500 | + # Converge & activate. |
501 | + message = twin |
502 | + message.shareIfPossible() |
503 | + elif character == '*': |
504 | + if share_with_other_side: |
505 | + if traits.other_side.incumbent_message is None: |
506 | + traits.other_side.setFlag(message, True) |
507 | + elif character == '+': |
508 | + if share_with_other_side: |
509 | + traits.other_side.setFlag(message, True) |
510 | + else: |
511 | + raise AssertionError( |
512 | + "Bad character in decision string: %s" % character) |
513 | + |
514 | + if decisions == '': |
515 | + message = twin |
516 | + |
517 | + traits.setFlag(message, True) |
518 | + |
519 | + return message |
520 | |
521 | def resetCurrentTranslation(self, pofile, lock_timestamp): |
522 | """See `IPOTMsgSet`.""" |
523 | @@ -1145,7 +1429,7 @@ |
524 | # The credits message has a fixed "translator." |
525 | translator = getUtility(ILaunchpadCelebrities).rosetta_experts |
526 | |
527 | - message = self.updateTranslation( |
528 | + self.updateTranslation( |
529 | pofile, translator, [credits_message_str], |
530 | is_current_upstream=False, allow_credits=True, |
531 | force_shared=True, force_edition_rights=True, |
532 | |
533 | === modified file 'lib/lp/translations/model/translationmessage.py' |
534 | --- lib/lp/translations/model/translationmessage.py 2010-05-07 12:29:23 +0000 |
535 | +++ lib/lp/translations/model/translationmessage.py 2010-06-21 16:42:29 +0000 |
536 | @@ -78,6 +78,11 @@ |
537 | forms = 2 |
538 | return forms |
539 | |
540 | + @property |
541 | + def is_diverged(self): |
542 | + """See `ITranslationMessage`.""" |
543 | + return self.potemplate is not None |
544 | + |
545 | def makeHTMLID(self, suffix=None): |
546 | """See `ITranslationMessage`.""" |
547 | elements = [self.language.code] |
548 | |
549 | === modified file 'lib/lp/translations/tests/test_potmsgset.py' |
550 | --- lib/lp/translations/tests/test_potmsgset.py 2010-06-09 14:17:32 +0000 |
551 | +++ lib/lp/translations/tests/test_potmsgset.py 2010-06-21 16:42:29 +0000 |
552 | @@ -25,6 +25,9 @@ |
553 | TranslationFileFormat) |
554 | from lp.translations.interfaces.translationmessage import ( |
555 | RosettaTranslationOrigin, TranslationConflict) |
556 | +from lp.translations.interfaces.translations import TranslationSide |
557 | +from lp.translations.model.potmsgset import ( |
558 | + make_translation_side_message_traits) |
559 | from lp.translations.model.translationmessage import ( |
560 | DummyTranslationMessage) |
561 | |
562 | @@ -1305,7 +1308,7 @@ |
563 | def test_creation_pofile(self): |
564 | # When a new pofile is created, dummy translations are created for |
565 | # all translation credits messages. |
566 | - |
567 | + |
568 | credits = self.factory.makePOTMsgSet( |
569 | self.potemplate, u'translator-credits', sequence=1) |
570 | eo_pofile = self.factory.makePOFile('eo', potemplate=self.potemplate) |
571 | @@ -1572,5 +1575,123 @@ |
572 | self.assertEqual([], karma_listener.karma_events) |
573 | |
574 | |
575 | +class TestSetCurrentTranslation(TestCaseWithFactory): |
576 | + layer = ZopelessDatabaseLayer |
577 | + |
578 | + def _makePOFileAndPOTMsgSet(self): |
579 | + pofile = self.factory.makePOFile('nl') |
580 | + potmsgset = self.factory.makePOTMsgSet(pofile.potemplate) |
581 | + return pofile, potmsgset |
582 | + |
583 | + def _makeTranslations(self, potmsgset, forms=1): |
584 | + return removeSecurityProxy(potmsgset)._findPOTranslations([ |
585 | + self.factory.getUniqueString() |
586 | + for counter in xrange(forms) |
587 | + ]) |
588 | + |
589 | + def test_baseline(self): |
590 | + # setCurrentTranslation sets the current upstream translation |
591 | + # for a message. |
592 | + pofile, potmsgset = self._makePOFileAndPOTMsgSet() |
593 | + translations = self._makeTranslations(potmsgset) |
594 | + origin = RosettaTranslationOrigin.SCM |
595 | + |
596 | + message = potmsgset.setCurrentTranslation( |
597 | + pofile, pofile.potemplate.owner, translations, origin, |
598 | + TranslationSide.UPSTREAM) |
599 | + |
600 | + self.assertEqual(message, potmsgset.getImportedTranslationMessage( |
601 | + pofile.potemplate, pofile.language)) |
602 | + self.assertEqual(origin, message.origin) |
603 | + |
604 | + def test_make_translation_side_message_traits(self): |
605 | + # make_translation_side_message_traits is a factory for traits |
606 | + # objects that help setCurrentTranslations deal with the |
607 | + # dichotomy between upstream and Ubuntu translations. |
608 | + pofile, potmsgset = self._makePOFileAndPOTMsgSet() |
609 | + sides = (TranslationSide.UPSTREAM, TranslationSide.UBUNTU) |
610 | + for side in sides: |
611 | + traits = make_translation_side_message_traits( |
612 | + side, potmsgset, pofile.potemplate, pofile.language) |
613 | + self.assertEqual(side, traits.side) |
614 | + self.assertNotEqual(side, traits.other_side.side) |
615 | + self.assertIn(traits.other_side.side, sides) |
616 | + self.assertIs(traits, traits.other_side.other_side) |
617 | + |
618 | + def test_UpstreamSideTraits_upstream(self): |
619 | + pofile, potmsgset = self._makePOFileAndPOTMsgSet() |
620 | + message = self.factory.makeTranslationMessage( |
621 | + pofile=pofile, potmsgset=potmsgset) |
622 | + |
623 | + traits = make_translation_side_message_traits( |
624 | + TranslationSide.UPSTREAM, potmsgset, pofile.potemplate, |
625 | + pofile.language) |
626 | + |
627 | + self.assertEqual('is_current_upstream', traits.flag_name) |
628 | + |
629 | + self.assertFalse(traits.getFlag(message)) |
630 | + self.assertFalse(message.is_current_upstream) |
631 | + self.assertEquals(None, traits.incumbent_message) |
632 | + |
633 | + traits.setFlag(message, True) |
634 | + |
635 | + self.assertTrue(traits.getFlag(message)) |
636 | + self.assertTrue(message.is_current_upstream) |
637 | + self.assertEquals(message, traits.incumbent_message) |
638 | + |
639 | + def test_UpstreamSideTraits_ubuntu(self): |
640 | + pofile, potmsgset = self._makePOFileAndPOTMsgSet() |
641 | + message = self.factory.makeTranslationMessage( |
642 | + pofile=pofile, potmsgset=potmsgset) |
643 | + message.is_current_ubuntu = False |
644 | + |
645 | + traits = make_translation_side_message_traits( |
646 | + TranslationSide.UBUNTU, potmsgset, pofile.potemplate, |
647 | + pofile.language) |
648 | + |
649 | + self.assertEqual('is_current_ubuntu', traits.flag_name) |
650 | + |
651 | + self.assertFalse(traits.getFlag(message)) |
652 | + self.assertFalse(message.is_current_ubuntu) |
653 | + self.assertEquals(None, traits.incumbent_message) |
654 | + |
655 | + traits.setFlag(message, True) |
656 | + |
657 | + self.assertTrue(traits.getFlag(message)) |
658 | + self.assertTrue(message.is_current_ubuntu) |
659 | + self.assertEquals(message, traits.incumbent_message) |
660 | + |
661 | + def test_identical(self): |
662 | + # Setting the same message twice leaves the original as-is. |
663 | + pofile, potmsgset = self._makePOFileAndPOTMsgSet() |
664 | + translations = self._makeTranslations(potmsgset) |
665 | + |
666 | + first_message = potmsgset.setCurrentTranslation( |
667 | + pofile, pofile.potemplate.owner, translations, |
668 | + RosettaTranslationOrigin.ROSETTAWEB, TranslationSide.UPSTREAM) |
669 | + second_message = potmsgset.setCurrentTranslation( |
670 | + pofile, self.factory.makePerson(), translations, |
671 | + RosettaTranslationOrigin.SCM, TranslationSide.UPSTREAM) |
672 | + |
673 | + self.assertEqual(first_message, second_message) |
674 | + message = first_message |
675 | + self.assertEqual(pofile.potemplate.owner, message.submitter) |
676 | + self.assertEqual(RosettaTranslationOrigin.ROSETTAWEB, message.origin) |
677 | + |
678 | + def test_upstream_also_sets_initial_ubuntu(self): |
679 | + # Setting an upstream translation also initializes the Ubuntu |
680 | + # translation. |
681 | + pofile, potmsgset = self._makePOFileAndPOTMsgSet() |
682 | + translations = self._makeTranslations(potmsgset) |
683 | + origin = RosettaTranslationOrigin.ROSETTAWEB |
684 | + |
685 | + message = potmsgset.setCurrentTranslation( |
686 | + pofile, pofile.potemplate.owner, translations, origin, |
687 | + TranslationSide.UPSTREAM) |
688 | + |
689 | + self.assertEqual(message, potmsgset.getImportedTranslationMessage( |
690 | + pofile.potemplate, pofile.language)) |
691 | + |
692 | + |
693 | def test_suite(): |
694 | return unittest.TestLoader().loadTestsFromName(__name__) |
695 | |
696 | === modified file 'lib/lp/translations/tests/test_translationmessage.py' |
697 | --- lib/lp/translations/tests/test_translationmessage.py 2009-08-25 20:15:38 +0000 |
698 | +++ lib/lp/translations/tests/test_translationmessage.py 2010-06-21 16:42:29 +0000 |
699 | @@ -1,4 +1,4 @@ |
700 | -# Copyright 2009 Canonical Ltd. This software is licensed under the |
701 | +# Copyright 2009-2010 Canonical Ltd. This software is licensed under the |
702 | # GNU Affero General Public License version 3 (see the file LICENSE). |
703 | |
704 | """Unit tests for `TranslationMessage`.""" |
705 | @@ -10,12 +10,48 @@ |
706 | from zope.component import getUtility |
707 | from zope.security.proxy import removeSecurityProxy |
708 | |
709 | +from canonical.launchpad.webapp.testing import verifyObject |
710 | from lp.services.worlddata.interfaces.language import ILanguageSet |
711 | from lp.testing import TestCaseWithFactory |
712 | from lp.translations.model.potranslation import POTranslation |
713 | +from lp.translations.model.translationmessage import DummyTranslationMessage |
714 | +from lp.translations.interfaces.translationmessage import ( |
715 | + ITranslationMessage) |
716 | from lp.translations.interfaces.translations import TranslationConstants |
717 | from canonical.testing import ZopelessDatabaseLayer |
718 | |
719 | + |
720 | +class TestTranslationMessage(TestCaseWithFactory): |
721 | + """Unit tests for `TranslationMessage`. |
722 | + |
723 | + There aren't many of these. We didn't do much unit testing back |
724 | + then. |
725 | + """ |
726 | + |
727 | + layer = ZopelessDatabaseLayer |
728 | + |
729 | + def test_baseline(self): |
730 | + message = self.factory.makeTranslationMessage() |
731 | + verifyObject(ITranslationMessage, message) |
732 | + |
733 | + def test_dummy_translationmessage(self): |
734 | + pofile = self.factory.makePOFile('nl') |
735 | + potmsgset = self.factory.makePOTMsgSet(pofile.potemplate) |
736 | + dummy = DummyTranslationMessage(pofile, potmsgset) |
737 | + verifyObject(ITranslationMessage, dummy) |
738 | + |
739 | + def test_is_diverged_false(self): |
740 | + # ITranslationMessage.is_diverged is a little helper to let you |
741 | + # say "message.is_diverged" which can be clearer than |
742 | + # "message.potemplate is not None." |
743 | + message = self.factory.makeTranslationMessage(force_diverged=False) |
744 | + self.assertFalse(message.is_diverged) |
745 | + |
746 | + def test_is_diverged_true(self): |
747 | + message = self.factory.makeTranslationMessage(force_diverged=True) |
748 | + self.assertTrue(message.is_diverged) |
749 | + |
750 | + |
751 | class TestTranslationMessageFindIdenticalMessage(TestCaseWithFactory): |
752 | """Tests for `TranslationMessage.findIdenticalMessage`.""" |
753 |
The "real" tests are being written separately by danilo. I only included some because I couldn't bear not to do it.
The code follows the decision matrix cited in the spec. There's ways to make it prettier, but we'll get to that sometime after we have good tests.
Jeroen