Merge lp:~jtv/launchpad/recife-552639 into lp:~launchpad/launchpad/recife

Proposed by Jeroen T. Vermeulen
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
Reviewer Review Type Date Requested Status
Henning Eggers (community) code Approve
Review via email: mp+24883@code.launchpad.net

Commit message

Implement setCurrentTranslation methods.

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://dev.launchpad.net/Translations/Specs/UpstreamImportIntoUbuntu/FixingIsImported/setCurrentTranslation#preview

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

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

Revision history for this message
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 resetCurrentTranslation tests, which are unrelated to this code. Henning has already fixed those in the main feature branch.

I'm going to put up the test suite for separate review, in order to keep line count in check.

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

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

> === modified file 'database/schema/security.cfg'
OK
> === modified file 'lib/lp/translations/interfaces/potmsgset.py'
> --- lib/lp/translations/interfaces/potmsgset.py 2010-06-09 14:17:32 +0000
> +++ lib/lp/translations/interfaces/potmsgset.py 2010-06-17 09:25:53 +0000
> @@ -214,6 +214,10 @@
> force_edition_rights=False, allow_credits=False):
> """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 setCurrentTranslation(pofile, submitter, translations, origin,
> + translation_side, share_with_other_side=False):
> + """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_other_side:
> + """
> + # XXX: Check signature before completing branch.
> + # XXX: Update docstring.

So, these XXX are supposed to be gone before we land the feature, right?

> +
> def resetCurrentTranslation(pofile, lock_timestamp):
> """Reset the currently used translation.
>
> === modified file 'lib/lp/translations/interfaces/translations.py'
OK

> === modified file 'lib/lp/translations/model/potmsgset.py'
> --- lib/lp/translations/model/potmsgset.py 2010-06-10 11:24:51 +0000
> +++ lib/lp/translations/model/potmsgset.py 2010-06-17 09:25:53 +0000
> @@ -4,11 +4,16 @@
> # pylint: disable-msg=E0611,W0212
>
> __metaclass__ = type
> -__all__ = ['POTMsgSet']
> +__all__ = [
> + 'make_translation_side_message_traits',
> + 'POTMsgSet',
> + ]
> +
>
> import datetime
> import logging
> import pytz
> +import re
>
> from zope.interface import implements
> from zope.component import getUtility
> @@ -41,7 +46,8 @@
> RosettaTranslationOrigin,
> TranslationConflict,
> TranslationValidationStatus)
> -from lp.translations.interfaces.translations import TranslationConstants
> +from lp.translations.interfaces.translations import (
> + TranslationConstants, TranslationSide)
> from lp.translations.model.pomsgid import POMsgID
> from lp.translations.model.potranslation import POTranslation
> from lp.translations.model.translationmessage import (
> @@ -79,6 +85,91 @@
> u'credits are counted as translated.')
>
>
> +class TranslationSideMessageTraits:
> + """Dealing with a `POTMsgSet` on either `TranslationSide`.
> +
> + Encapsulates primitives that depend on translation side: finding the
> + message that ...

review: Needs Fixing (code)
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :
Download full text (17.7 KiB)

> > === modified file 'lib/lp/translations/interfaces/potmsgset.py'
> > --- lib/lp/translations/interfaces/potmsgset.py 2010-06-09 14:17:32
> +0000
> > +++ lib/lp/translations/interfaces/potmsgset.py 2010-06-17 09:25:53
> +0000
> > @@ -214,6 +214,10 @@
> > force_edition_rights=False, allow_credits=False):
> > """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 setCurrentTranslation(pofile, submitter, translations, origin,
> > + translation_side,
> share_with_other_side=False):
> > + """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_other_side:
> > + """
> > + # 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/translations/model/potmsgset.py'
> > --- lib/lp/translations/model/potmsgset.py 2010-06-10 11:24:51 +0000
> > +++ lib/lp/translations/model/potmsgset.py 2010-06-17 09:25:53 +0000
> > @@ -4,11 +4,16 @@
> > # pylint: disable-msg=E0611,W0212
> >
> > __metaclass__ = type
> > -__all__ = ['POTMsgSet']
> > +__all__ = [
> > + 'make_translation_side_message_traits',
> > + 'POTMsgSet',
> > + ]
> > +
> >
> > import datetime
> > import logging
> > import pytz
> > +import re
> >
> > from zope.interface import implements
> > from zope.component import getUtility
> > @@ -41,7 +46,8 @@
> > RosettaTranslationOrigin,
> > TranslationConflict,
> > TranslationValidationStatus)
> > -from lp.translations.interfaces.translations import TranslationConstants
> > +from lp.translations.interfaces.translations import (
> > + TranslationConstants, TranslationSide)
> > from lp.translations.model.pomsgid import POMsgID
> > from lp.translations.model.potranslation import POTranslation
> > from lp.translations.model.translationmessage import (
> > @@ -79,6 +85,91 @@
> > u'credits are counted as translated.')
> >
> >
> > +class TranslationSideMessageTraits:
> > + """Dealing with a `POTMsgSet` on either `Translatio...

Revision history for this message
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._makeTranslationMessage(
> > + pofile, submitter, translations, origin)
> > + elif character == '2':
> > + # Create, diverge, activate.
> > + message = self._makeTranslationMessage(
> > + 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.getFlag(twin):
> > + assert not traits.other_side.getFlag(twin), (
> > + "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.setFlag(message, True)
> > +
> > + return message
> > +
> > +>>>>>>> MERGE-SOURCE
> > def resetCurrentTranslation(self, pofile, lock_timestamp):
> > """See `IPOTMsgSet`."""
> >
> > @@ -1145,7 +1423,7 @@
> > # The credits message has a fixed "translator."
> > translator = getUtility(ILaunchpadCelebrities).rosetta_experts
> >
> > - message = self.updateTranslation(
> > + self.updateTranslation(
> > pofile, translator, [credits_message_str],
> > is_current_upstream=False, allow_credits=True,
> > force_shared=True, force_edition_rights=True,
> > === modified file 'lib/lp/translations/tests/test_potmsgset.py'
>
> 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

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

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

Am 19.06.2010 07:27, schrieb Jeroen T. Vermeulen:
[...]
>>> +=======
>>> + def _nameMessageStatus(self, message, translation_side_traits):
>>> + """Figure out the decision-matrix status of a message.
>>> +
>>> + This is used in navigating the decision matrix in
>>> + `setCurrentTranslation`.
>>> + """
>>> + if message is None:
>>> + return 'none'
>>> + elif message.potemplate is None:
>>> + if translation_side_traits.other_side.getFlag(message):
>>> + 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 ITranslationMessage.is_diverged to allow for clearer checks.
> 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 setCurrentTranslation(self, pofile, submitter, translations,
>> origin,
>>> + translation_side,
>> share_with_other_side=False):
>>> + """See `IPOTMsgSet`."""
>>> + traits = make_translation_side_message_traits(
>>> + translation_side, self, pofile.potemplate, pofile.language,
>>> + variant=pofile.variant)
>>> +
>>> + translations = self._findPOTranslations(translations)
>>> + incumbent_message = traits.incumbent_message
>>> + twin = self._findTranslationMessage(
>>> + pofile, translations, prefer_shared=False)
>>
>> 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,
>>> +
>...

Read more...

review: Approve (code)
Revision history for this message
Henning Eggers (henninge) wrote :

Here one comment, I forgot.

> === modified file 'lib/lp/translations/tests/test_translationmessage.py'
> --- lib/lp/translations/tests/test_translationmessage.py 2009-08-25 20:15:38 +0000
> +++ lib/lp/translations/tests/test_translationmessage.py 2010-06-19 03:38:37 +0000
> @@ -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 `TranslationMessage`."""
> @@ -10,12 +10,46 @@
> from zope.component import getUtility
> from zope.security.proxy import removeSecurityProxy
>
> +from canonical.launchpad.webapp.testing import verifyObject
> from lp.services.worlddata.interfaces.language import ILanguageSet
> from lp.testing import TestCaseWithFactory
> from lp.translations.model.potranslation import POTranslation
> +from lp.translations.model.translationmessage import DummyTranslationMessage
> +from lp.translations.interfaces.translationmessage import (
> + ITranslationMessage)
> from lp.translations.interfaces.translations import TranslationConstants
> from canonical.testing import ZopelessDatabaseLayer
>
> +
> +class TestTranslationMessage(TestCaseWithFactory):
> + """Unit tests for `TranslationMessage`.
> +
> + There aren't many of these. We didn't do much unit testing back
> + then.
> + """

And thanks a lot for adding these! ;-)

> +
> + layer = ZopelessDatabaseLayer
> +
> + def test_baseline(self):
> + message = self.factory.makeTranslationMessage()
> + verifyObject(ITranslationMessage, message)

I'd have split this test here.

> +
> + pofile = self.factory.makePOFile('nl')
> + potmsgset = self.factory.makePOTMsgSet(pofile.potemplate)
> + dummy = DummyTranslationMessage(pofile, potmsgset)
> + verifyObject(ITranslationMessage, dummy)
> +
> + def test_is_diverged(self):
> + # ITranslationMessage.is_diverged is a little helper to let you
> + # say "message.is_diverged" which can be clearer than
> + # "message.potemplate is not None."
> + message = self.factory.makeTranslationMessage(force_diverged=False)
> + self.assertFalse(message.is_diverged)
> +

And this one here.

> + message = self.factory.makeTranslationMessage(force_diverged=True)
> + self.assertTrue(message.is_diverged)

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 TestTranslationMessageFindIdenticalMessage(TestCaseWithFactory):
> """Tests for `TranslationMessage.findIdenticalMessage`."""
>

Cheers, Henning

Revision history for this message
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/translations/tests/test_translationmessage.py'
> > --- lib/lp/translations/tests/test_translationmessage.py 2009-08-25
> 20:15:38 +0000
> > +++ lib/lp/translations/tests/test_translationmessage.py 2010-06-19
> 03:38:37 +0000

> > """Unit tests for `TranslationMessage`."""
> > + def test_baseline(self):
> > + message = self.factory.makeTranslationMessage()
> > + verifyObject(ITranslationMessage, message)
>
> 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_diverged(self):
> > + # ITranslationMessage.is_diverged is a little helper to let you
> > + # say "message.is_diverged" which can be clearer than
> > + # "message.potemplate is not None."
> > + message = self.factory.makeTranslationMessage(force_diverged=False)
> > + self.assertFalse(message.is_diverged)
> > +
>
> And this one here.

Both split now.

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-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

Subscribers

People subscribed via source and target branches