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
=== modified file 'database/schema/security.cfg'
--- database/schema/security.cfg 2010-05-20 13:59:11 +0000
+++ database/schema/security.cfg 2010-06-21 16:42:29 +0000
@@ -258,7 +258,7 @@
258public.temporaryblobstorage = SELECT, INSERT, DELETE258public.temporaryblobstorage = SELECT, INSERT, DELETE
259public.translationgroup = SELECT, INSERT, UPDATE259public.translationgroup = SELECT, INSERT, UPDATE
260public.translationimportqueueentry = SELECT, INSERT, UPDATE, DELETE260public.translationimportqueueentry = SELECT, INSERT, UPDATE, DELETE
261public.translationmessage = SELECT, INSERT, UPDATE261public.translationmessage = SELECT, INSERT, UPDATE, DELETE
262public.translator = SELECT, INSERT, UPDATE, DELETE262public.translator = SELECT, INSERT, UPDATE, DELETE
263public.usertouseremail = SELECT, UPDATE263public.usertouseremail = SELECT, UPDATE
264public.validpersoncache = SELECT264public.validpersoncache = SELECT
265265
=== 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-21 16:42:29 +0000
@@ -214,6 +214,10 @@
214 force_edition_rights=False, allow_credits=False):214 force_edition_rights=False, allow_credits=False):
215 """Update or create a translation message using `new_translations`.215 """Update or create a translation message using `new_translations`.
216216
217 This method is Launchpad Translations's sliderule: it does
218 everything, nobody fully understands it all, and we intend to
219 replace it with a range of less flexible tools.
220
217 :param pofile: a `POFile` to add `new_translations` to.221 :param pofile: a `POFile` to add `new_translations` to.
218 :param submitter: author of the translations.222 :param submitter: author of the translations.
219 :param new_translations: a dictionary of plural forms, with the223 :param new_translations: a dictionary of plural forms, with the
@@ -261,6 +265,23 @@
261 If a translation conflict is detected, TranslationConflict is raised.265 If a translation conflict is detected, TranslationConflict is raised.
262 """266 """
263267
268 def setCurrentTranslation(pofile, submitter, translations, origin,
269 translation_side, share_with_other_side=False):
270 """Set the message's translation in Ubuntu, or upstream, or both.
271
272 :param pofile: `POFile` you're setting translations in. Other
273 `POFiles` that share translations with this one may also be
274 affected.
275 :param submitter: `Person` who is setting these translations.
276 :param translations: a dict mapping plural-form numbers to the
277 translated string for that form.
278 :param origin: A `RosettaTranslationOrigin`.
279 :param translation_side: The `TranslationSide` that this
280 translation is for (Ubuntu or upstream).
281 :param share_with_other_side: When sharing this translation,
282 share it with the other `TranslationSide` as well.
283 """
284
264 def resetCurrentTranslation(pofile, lock_timestamp):285 def resetCurrentTranslation(pofile, lock_timestamp):
265 """Reset the currently used translation.286 """Reset the currently used translation.
266287
267288
=== modified file 'lib/lp/translations/interfaces/translationmessage.py'
--- lib/lp/translations/interfaces/translationmessage.py 2010-03-26 19:49:46 +0000
+++ lib/lp/translations/interfaces/translationmessage.py 2010-06-21 16:42:29 +0000
@@ -209,6 +209,9 @@
209 title=_("Number of plural form translations in this translation."),209 title=_("Number of plural form translations in this translation."),
210 readonly=True, required=True)210 readonly=True, required=True)
211211
212 is_diverged = Bool(
213 title=_("Is this message diverged?"), readonly=True, required=True)
214
212 def setPOFile(pofile):215 def setPOFile(pofile):
213 """Set a POFile for use in views."""216 """Set a POFile for use in views."""
214217
215218
=== modified file 'lib/lp/translations/interfaces/translations.py'
--- lib/lp/translations/interfaces/translations.py 2009-07-17 00:26:05 +0000
+++ lib/lp/translations/interfaces/translations.py 2010-06-21 16:42:29 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009 Canonical Ltd. This software is licensed under the1# Copyright 2009-2010 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4# pylint: disable-msg=E0211,E02134# pylint: disable-msg=E0211,E0213
@@ -7,10 +7,12 @@
77
8__metaclass__ = type8__metaclass__ = type
99
10__all__ = (10__all__ = [
11 'TranslationConstants',11 'TranslationConstants',
12 'TranslationsBranchImportMode',12 'TranslationsBranchImportMode',
13 )13 'TranslationSide',
14 ]
15
1416
15class TranslationConstants:17class TranslationConstants:
16 """Set of constants used inside the context of translations."""18 """Set of constants used inside the context of translations."""
@@ -52,3 +54,10 @@
52 """)54 """)
5355
5456
57class TranslationSide:
58 """The two "sides" of software that can be translated in Launchpad.
59
60 These are "upstream" and "Ubuntu."
61 """
62 UPSTREAM = 1
63 UBUNTU = 2
5564
=== 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-21 16:42:29 +0000
@@ -4,11 +4,16 @@
4# pylint: disable-msg=E0611,W02124# pylint: disable-msg=E0611,W0212
55
6__metaclass__ = type6__metaclass__ = type
7__all__ = ['POTMsgSet']7__all__ = [
8 'make_translation_side_message_traits',
9 'POTMsgSet',
10 ]
11
812
9import datetime13import datetime
10import logging14import logging
11import pytz15import pytz
16import re
1217
13from zope.interface import implements18from zope.interface import implements
14from zope.component import getUtility19from zope.component import getUtility
@@ -41,7 +46,8 @@
41 RosettaTranslationOrigin,46 RosettaTranslationOrigin,
42 TranslationConflict,47 TranslationConflict,
43 TranslationValidationStatus)48 TranslationValidationStatus)
44from lp.translations.interfaces.translations import TranslationConstants49from lp.translations.interfaces.translations import (
50 TranslationConstants, TranslationSide)
45from lp.translations.model.pomsgid import POMsgID51from lp.translations.model.pomsgid import POMsgID
46from lp.translations.model.potranslation import POTranslation52from lp.translations.model.potranslation import POTranslation
47from lp.translations.model.translationmessage import (53from lp.translations.model.translationmessage import (
@@ -79,6 +85,100 @@
79 u'credits are counted as translated.')85 u'credits are counted as translated.')
8086
8187
88class TranslationSideMessageTraits:
89 """Dealing with a `POTMsgSet` on either `TranslationSide`.
90
91 Encapsulates primitives that depend on translation side: finding the
92 message that is current on the given side, checking the flag that
93 says whether a message is current on this side, setting or clearing
94 the flag, and providing the same capabilities for the other side.
95
96 For an introduction to the Traits pattern, see
97 http://www.cantrip.org/traits.html
98 """
99 # The TranslationSide that these Traits are for.
100 side = None
101
102 # TranslationSideMessageTraits for this message on the "other side."
103 other_side = None
104
105 # Name of this side's flag.
106 flag_name = None
107
108 def __init__(self, potmsgset, potemplate=None, language=None,
109 variant=None):
110 self.potmsgset = potmsgset
111 self.potemplate = potemplate
112 self.language = language
113 self.variant = variant
114
115 self._found_incumbent = False
116
117 @property
118 def incumbent_message(self):
119 """Message that currently has the flag."""
120 if not self._found_incumbent:
121 self._incumbent = self._getIncumbentMessage()
122 self._found_incumbent = True
123 return self._incumbent
124
125 def getFlag(self, translationmessage):
126 """Is this message the current one on this side?"""
127 return getattr(translationmessage, self.flag_name)
128
129 def setFlag(self, translationmessage, value):
130 """Set or clear a message's "current" flag for this side."""
131 if value == self.getFlag(translationmessage):
132 return
133
134 if value and self.incumbent_message is not None:
135 self.setFlag(self.incumbent_message, False)
136
137 setattr(translationmessage, self.flag_name, value)
138 self._found_incumbent = False
139
140 def _getIncumbentMessage(self):
141 """Get the message that is current on this side, if any."""
142 raise NotImplementedError('_getIncumbentMessage')
143
144
145class UpstreamSideTraits(TranslationSideMessageTraits):
146 """Traits for upstream translations."""
147
148 side = TranslationSide.UPSTREAM
149
150 flag_name = 'is_current_upstream'
151
152 def _getIncumbentMessage(self):
153 """See `TranslationSideMessageTraits`."""
154 return self.potmsgset.getImportedTranslationMessage(
155 self.potemplate, self.language, variant=self.variant)
156
157
158class UbuntuSideTraits(TranslationSideMessageTraits):
159 """Traits for Ubuntu translations."""
160
161 side = TranslationSide.UBUNTU
162
163 flag_name = 'is_current_ubuntu'
164
165 def _getIncumbentMessage(self):
166 """See `TranslationSideMessageTraits`."""
167 return self.potmsgset.getCurrentTranslationMessage(
168 self.potemplate, self.language, variant=self.variant)
169
170
171def make_translation_side_message_traits(side, potmsgset, potemplate,
172 language, variant=None):
173 """Create `TranslationSideTraits` object of the appropriate subtype."""
174 ubuntu = UbuntuSideTraits(potmsgset, potemplate, language, variant)
175 upstream = UpstreamSideTraits(potmsgset, potemplate, language, variant)
176 upstream.other_side = ubuntu
177 ubuntu.other_side = upstream
178 mapping = dict((traits.side, traits) for traits in (ubuntu, upstream))
179 return mapping[side]
180
181
82class POTMsgSet(SQLBase):182class POTMsgSet(SQLBase):
83 implements(IPOTMsgSet)183 implements(IPOTMsgSet)
84184
@@ -232,13 +332,13 @@
232 current=True):332 current=True):
233 """Get a translation message which is either used in333 """Get a translation message which is either used in
234 Launchpad (current=True) or in an import (current=False).334 Launchpad (current=True) or in an import (current=False).
235 335
236 Prefers a diverged message if present.336 Prefers a diverged message if present.
237 """337 """
238 # Change the is_current_ubuntu and is_current_upstream conditions338 # Change 'is_current IS TRUE' and 'is_imported IS TRUE' conditions
239 # with extreme care: they need to match condition specified in339 # carefully: they need to match condition specified in indexes,
240 # indexes, or postgres may not pick them up (in complicated queries,340 # or Postgres may not pick them up (in complicated queries,
241 # postgres query optimizer sometimes does text-matching of indexes).341 # Postgres query optimizer sometimes does text-matching of indexes).
242 if current:342 if current:
243 used_clause = 'is_current_ubuntu IS TRUE'343 used_clause = 'is_current_ubuntu IS TRUE'
244 else:344 else:
@@ -522,13 +622,17 @@
522 potranslations[pluralform] = None622 potranslations[pluralform] = None
523 return potranslations623 return potranslations
524624
525 def _findTranslationMessage(self, pofile, potranslations, pluralforms):625 def _findTranslationMessage(self, pofile, potranslations,
526 """Find a message for this `pofile`.626 prefer_shared=True):
527627 """Find a matching message in this `pofile`.
528 The returned message matches exactly the given `translations` strings628
529 comparing only `pluralforms` of them.629 The returned message matches exactly the given `translations`
630 strings (except plural forms not supported by `pofile`, which
631 are ignored).
632
530 :param potranslations: A list of translation strings.633 :param potranslations: A list of translation strings.
531 :param pluralforms: The number of pluralforms to compare.634 :param prefer_shared: Whether to prefer a shared match over a
635 diverged one.
532 """636 """
533 clauses = ['potmsgset = %s' % sqlvalues(self),637 clauses = ['potmsgset = %s' % sqlvalues(self),
534 'language = %s' % sqlvalues(pofile.language),638 'language = %s' % sqlvalues(pofile.language),
@@ -539,7 +643,7 @@
539 else:643 else:
540 clauses.append('variant = %s' % sqlvalues(pofile.variant))644 clauses.append('variant = %s' % sqlvalues(pofile.variant))
541645
542 for pluralform in range(pluralforms):646 for pluralform in range(pofile.plural_forms):
543 if potranslations[pluralform] is None:647 if potranslations[pluralform] is None:
544 clauses.append('msgstr%s IS NULL' % sqlvalues(pluralform))648 clauses.append('msgstr%s IS NULL' % sqlvalues(pluralform))
545 else:649 else:
@@ -547,10 +651,15 @@
547 sqlvalues(pluralform, potranslations[pluralform])))651 sqlvalues(pluralform, potranslations[pluralform])))
548652
549 remaining_plural_forms = range(653 remaining_plural_forms = range(
550 pluralforms, TranslationConstants.MAX_PLURAL_FORMS)654 pofile.plural_forms, TranslationConstants.MAX_PLURAL_FORMS)
551655
552 # Prefer shared messages over diverged ones.656 # Prefer either shared or diverged messages, depending on
553 order = ['potemplate NULLS FIRST']657 # arguments.
658 if prefer_shared:
659 order = ['potemplate NULLS FIRST']
660 else:
661 order = ['potemplate NULLS LAST']
662
554 # Normally at most one message should match. But if there is663 # Normally at most one message should match. But if there is
555 # more than one, prefer the one that adds the fewest extraneous664 # more than one, prefer the one that adds the fewest extraneous
556 # plural forms.665 # plural forms.
@@ -564,9 +673,9 @@
564 if len(matches) > 0:673 if len(matches) > 0:
565 if len(matches) > 1:674 if len(matches) > 1:
566 logging.warn(675 logging.warn(
567 "Translation for POTMsgSet %s into %s "676 "Translation for POTMsgSet %s into '%s' "
568 "matches %s existing translations." % sqlvalues(677 "matches %s existing translations.",
569 self, pofile.language.code, len(matches)))678 self.id, pofile.language.code, len(matches))
570 return matches[0]679 return matches[0]
571 else:680 else:
572 return None681 return None
@@ -799,7 +908,7 @@
799 # of translations. None if there is no such message and needs to be908 # of translations. None if there is no such message and needs to be
800 # created.909 # created.
801 matching_message = self._findTranslationMessage(910 matching_message = self._findTranslationMessage(
802 pofile, potranslations, pofile.plural_forms)911 pofile, potranslations)
803912
804 match_is_upstream = (913 match_is_upstream = (
805 matching_message is not None and914 matching_message is not None and
@@ -897,7 +1006,7 @@
897 potranslations = self._findPOTranslations(new_translations)1006 potranslations = self._findPOTranslations(new_translations)
8981007
899 existing_message = self._findTranslationMessage(1008 existing_message = self._findTranslationMessage(
900 pofile, potranslations, pofile.plural_forms)1009 pofile, potranslations)
901 if existing_message is not None:1010 if existing_message is not None:
902 return existing_message1011 return existing_message
9031012
@@ -951,9 +1060,184 @@
951 current.reviewer = reviewer1060 current.reviewer = reviewer
952 current.date_reviewed = lock_timestamp1061 current.date_reviewed = lock_timestamp
9531062
1063 def _nameMessageStatus(self, message, translation_side_traits):
1064 """Figure out the decision-matrix status of a message.
1065
1066 This is used in navigating the decision matrix in
1067 `setCurrentTranslation`.
1068 """
1069 if message is None:
1070 return 'none'
1071 elif message.is_diverged:
1072 return 'diverged'
1073 elif translation_side_traits.other_side.getFlag(message):
1074 return 'other_shared'
1075 else:
1076 return 'shared'
1077
1078 def _makeTranslationMessage(self, pofile, submitter, translations, origin,
1079 diverged=False):
1080 """Create a new `TranslationMessage`.
1081
1082 The message will not be made current on either side (Ubuntu or
1083 upstream), but it can be diverged. Only messages that are
1084 current should be diverged, but it's up to the caller to ensure
1085 the right state.
1086 """
1087 if diverged:
1088 potemplate = pofile.potemplate
1089 else:
1090 potemplate = None
1091
1092 translation_args = dict(
1093 ('msgstr%d' % form, translation)
1094 for form, translation in translations.iteritems()
1095 )
1096
1097 return TranslationMessage(
1098 potmsgset=self,
1099 potemplate=potemplate,
1100 pofile=pofile,
1101 language=pofile.language,
1102 variant=pofile.variant,
1103 origin=origin,
1104 submitter=submitter,
1105 validation_status=TranslationValidationStatus.OK,
1106 **translation_args)
1107
954 def setCurrentTranslation(self, pofile, submitter, translations, origin,1108 def setCurrentTranslation(self, pofile, submitter, translations, origin,
955 translation_side, share_with_other_side=False):1109 translation_side, share_with_other_side=False):
956 """See `IPOTMsgSet`."""1110 """See `IPOTMsgSet`."""
1111 traits = make_translation_side_message_traits(
1112 translation_side, self, pofile.potemplate, pofile.language,
1113 variant=pofile.variant)
1114
1115 translations = self._findPOTranslations(translations)
1116
1117 # The current message on this translation side, if any.
1118 incumbent_message = traits.incumbent_message
1119
1120 # An already existing message, if any, that's either shared, or
1121 # diverged for the template/pofile we're working on, whose
1122 # translations are identical to the ones we're setting.
1123 twin = self._findTranslationMessage(
1124 pofile, translations, prefer_shared=False)
1125
1126 # Summary of the matrix:
1127 # * If the incumbent message is diverged and we're setting a
1128 # translation that's already shared: converge.
1129 # * If the incumbent message is diverged and we're setting a
1130 # translation that's not already shared: maintain divergence.
1131 # * If the incumbent message is shared, replace it.
1132 # * If there is no twin, simply create a new message (shared or
1133 # diverged depending; see above).
1134 # * If there is a shared twin, activate it (but also diverge if
1135 # necessary; see above).
1136 # * If there is a diverged twin, activate it (and converge it
1137 # if appropriate; see above).
1138 # * If there is a twin that's shared on the other side,
1139
1140 decision_matrix = {
1141 'incumbent_none': {
1142 'twin_none': 'Z1*',
1143 'twin_shared': 'Z4*',
1144 'twin_diverged': 'Z7*',
1145 'twin_other_shared': 'Z4',
1146 },
1147 'incumbent_shared': {
1148 'twin_none': 'B1*',
1149 'twin_shared': 'B4*',
1150 'twin_diverged': 'B7*',
1151 'twin_other_shared': 'B4',
1152 },
1153 'incumbent_diverged': {
1154 'twin_none': 'A2',
1155 'twin_shared': 'A5',
1156 'twin_diverged': 'A4',
1157 'twin_other_shared': 'A6',
1158 },
1159 'incumbent_other_shared': {
1160 'twin_none': 'B1+',
1161 'twin_shared': 'B4+',
1162 'twin_diverged': 'B7+',
1163 'twin_other_shared': '',
1164 },
1165 }
1166
1167 incumbent_state = "incumbent_%s" % self._nameMessageStatus(
1168 incumbent_message, traits)
1169 twin_state = "twin_%s" % self._nameMessageStatus(twin, traits)
1170
1171 decisions = decision_matrix[incumbent_state][twin_state]
1172 assert re.match('[ABZ]?[124567]?[+*]?$', decisions), (
1173 "Bad decision string.")
1174
1175 for character in decisions:
1176 if character == 'A':
1177 # Deactivate & converge.
1178 # There may be an identical shared message.
1179 traits.setFlag(incumbent_message, False)
1180 incumbent_message.shareIfPossible()
1181 elif character == 'B':
1182 # Deactivate.
1183 traits.setFlag(incumbent_message, False)
1184 elif character == 'Z':
1185 # There is no incumbent message, so do nothing to it.
1186 assert incumbent_message is None, (
1187 "Incorrect Z in decision matrix.")
1188 elif character == '1':
1189 # Create & activate.
1190 message = self._makeTranslationMessage(
1191 pofile, submitter, translations, origin)
1192 elif character == '2':
1193 # Create, diverge, activate.
1194 message = self._makeTranslationMessage(
1195 pofile, submitter, translations, origin, diverged=True)
1196 elif character == '4':
1197 # Activate.
1198 message = twin
1199 elif character == '5':
1200 # If other is a suggestion, diverge and activate.
1201 # (If not, it's already active and has been unmasked by
1202 # our deactivating the incumbent).
1203 message = twin
1204 if not traits.getFlag(twin):
1205 assert not traits.other_side.getFlag(twin), (
1206 "Trying to diverge a message that is current on the "
1207 "other side.")
1208 message.potemplate = pofile.potemplate
1209 elif character == '6':
1210 # If other is not active, fork a diverged message.
1211 if traits.getFlag(twin):
1212 message = twin
1213 else:
1214 # The twin is used on the other side, so we can't
1215 # just reuse it for our diverged message. Create a
1216 # new one.
1217 message = self._makeTranslationMessage(
1218 pofile, submitter, translations, origin,
1219 diverged=True)
1220 elif character == '7':
1221 # Converge & activate.
1222 message = twin
1223 message.shareIfPossible()
1224 elif character == '*':
1225 if share_with_other_side:
1226 if traits.other_side.incumbent_message is None:
1227 traits.other_side.setFlag(message, True)
1228 elif character == '+':
1229 if share_with_other_side:
1230 traits.other_side.setFlag(message, True)
1231 else:
1232 raise AssertionError(
1233 "Bad character in decision string: %s" % character)
1234
1235 if decisions == '':
1236 message = twin
1237
1238 traits.setFlag(message, True)
1239
1240 return message
9571241
958 def resetCurrentTranslation(self, pofile, lock_timestamp):1242 def resetCurrentTranslation(self, pofile, lock_timestamp):
959 """See `IPOTMsgSet`."""1243 """See `IPOTMsgSet`."""
@@ -1145,7 +1429,7 @@
1145 # The credits message has a fixed "translator."1429 # The credits message has a fixed "translator."
1146 translator = getUtility(ILaunchpadCelebrities).rosetta_experts1430 translator = getUtility(ILaunchpadCelebrities).rosetta_experts
11471431
1148 message = self.updateTranslation(1432 self.updateTranslation(
1149 pofile, translator, [credits_message_str],1433 pofile, translator, [credits_message_str],
1150 is_current_upstream=False, allow_credits=True,1434 is_current_upstream=False, allow_credits=True,
1151 force_shared=True, force_edition_rights=True,1435 force_shared=True, force_edition_rights=True,
11521436
=== modified file 'lib/lp/translations/model/translationmessage.py'
--- lib/lp/translations/model/translationmessage.py 2010-05-07 12:29:23 +0000
+++ lib/lp/translations/model/translationmessage.py 2010-06-21 16:42:29 +0000
@@ -78,6 +78,11 @@
78 forms = 278 forms = 2
79 return forms79 return forms
8080
81 @property
82 def is_diverged(self):
83 """See `ITranslationMessage`."""
84 return self.potemplate is not None
85
81 def makeHTMLID(self, suffix=None):86 def makeHTMLID(self, suffix=None):
82 """See `ITranslationMessage`."""87 """See `ITranslationMessage`."""
83 elements = [self.language.code]88 elements = [self.language.code]
8489
=== modified file 'lib/lp/translations/tests/test_potmsgset.py'
--- lib/lp/translations/tests/test_potmsgset.py 2010-06-09 14:17:32 +0000
+++ lib/lp/translations/tests/test_potmsgset.py 2010-06-21 16:42:29 +0000
@@ -25,6 +25,9 @@
25 TranslationFileFormat)25 TranslationFileFormat)
26from lp.translations.interfaces.translationmessage import (26from lp.translations.interfaces.translationmessage import (
27 RosettaTranslationOrigin, TranslationConflict)27 RosettaTranslationOrigin, TranslationConflict)
28from lp.translations.interfaces.translations import TranslationSide
29from lp.translations.model.potmsgset import (
30 make_translation_side_message_traits)
28from lp.translations.model.translationmessage import (31from lp.translations.model.translationmessage import (
29 DummyTranslationMessage)32 DummyTranslationMessage)
3033
@@ -1305,7 +1308,7 @@
1305 def test_creation_pofile(self):1308 def test_creation_pofile(self):
1306 # When a new pofile is created, dummy translations are created for1309 # When a new pofile is created, dummy translations are created for
1307 # all translation credits messages.1310 # all translation credits messages.
1308 1311
1309 credits = self.factory.makePOTMsgSet(1312 credits = self.factory.makePOTMsgSet(
1310 self.potemplate, u'translator-credits', sequence=1)1313 self.potemplate, u'translator-credits', sequence=1)
1311 eo_pofile = self.factory.makePOFile('eo', potemplate=self.potemplate)1314 eo_pofile = self.factory.makePOFile('eo', potemplate=self.potemplate)
@@ -1572,5 +1575,123 @@
1572 self.assertEqual([], karma_listener.karma_events)1575 self.assertEqual([], karma_listener.karma_events)
15731576
15741577
1578class TestSetCurrentTranslation(TestCaseWithFactory):
1579 layer = ZopelessDatabaseLayer
1580
1581 def _makePOFileAndPOTMsgSet(self):
1582 pofile = self.factory.makePOFile('nl')
1583 potmsgset = self.factory.makePOTMsgSet(pofile.potemplate)
1584 return pofile, potmsgset
1585
1586 def _makeTranslations(self, potmsgset, forms=1):
1587 return removeSecurityProxy(potmsgset)._findPOTranslations([
1588 self.factory.getUniqueString()
1589 for counter in xrange(forms)
1590 ])
1591
1592 def test_baseline(self):
1593 # setCurrentTranslation sets the current upstream translation
1594 # for a message.
1595 pofile, potmsgset = self._makePOFileAndPOTMsgSet()
1596 translations = self._makeTranslations(potmsgset)
1597 origin = RosettaTranslationOrigin.SCM
1598
1599 message = potmsgset.setCurrentTranslation(
1600 pofile, pofile.potemplate.owner, translations, origin,
1601 TranslationSide.UPSTREAM)
1602
1603 self.assertEqual(message, potmsgset.getImportedTranslationMessage(
1604 pofile.potemplate, pofile.language))
1605 self.assertEqual(origin, message.origin)
1606
1607 def test_make_translation_side_message_traits(self):
1608 # make_translation_side_message_traits is a factory for traits
1609 # objects that help setCurrentTranslations deal with the
1610 # dichotomy between upstream and Ubuntu translations.
1611 pofile, potmsgset = self._makePOFileAndPOTMsgSet()
1612 sides = (TranslationSide.UPSTREAM, TranslationSide.UBUNTU)
1613 for side in sides:
1614 traits = make_translation_side_message_traits(
1615 side, potmsgset, pofile.potemplate, pofile.language)
1616 self.assertEqual(side, traits.side)
1617 self.assertNotEqual(side, traits.other_side.side)
1618 self.assertIn(traits.other_side.side, sides)
1619 self.assertIs(traits, traits.other_side.other_side)
1620
1621 def test_UpstreamSideTraits_upstream(self):
1622 pofile, potmsgset = self._makePOFileAndPOTMsgSet()
1623 message = self.factory.makeTranslationMessage(
1624 pofile=pofile, potmsgset=potmsgset)
1625
1626 traits = make_translation_side_message_traits(
1627 TranslationSide.UPSTREAM, potmsgset, pofile.potemplate,
1628 pofile.language)
1629
1630 self.assertEqual('is_current_upstream', traits.flag_name)
1631
1632 self.assertFalse(traits.getFlag(message))
1633 self.assertFalse(message.is_current_upstream)
1634 self.assertEquals(None, traits.incumbent_message)
1635
1636 traits.setFlag(message, True)
1637
1638 self.assertTrue(traits.getFlag(message))
1639 self.assertTrue(message.is_current_upstream)
1640 self.assertEquals(message, traits.incumbent_message)
1641
1642 def test_UpstreamSideTraits_ubuntu(self):
1643 pofile, potmsgset = self._makePOFileAndPOTMsgSet()
1644 message = self.factory.makeTranslationMessage(
1645 pofile=pofile, potmsgset=potmsgset)
1646 message.is_current_ubuntu = False
1647
1648 traits = make_translation_side_message_traits(
1649 TranslationSide.UBUNTU, potmsgset, pofile.potemplate,
1650 pofile.language)
1651
1652 self.assertEqual('is_current_ubuntu', traits.flag_name)
1653
1654 self.assertFalse(traits.getFlag(message))
1655 self.assertFalse(message.is_current_ubuntu)
1656 self.assertEquals(None, traits.incumbent_message)
1657
1658 traits.setFlag(message, True)
1659
1660 self.assertTrue(traits.getFlag(message))
1661 self.assertTrue(message.is_current_ubuntu)
1662 self.assertEquals(message, traits.incumbent_message)
1663
1664 def test_identical(self):
1665 # Setting the same message twice leaves the original as-is.
1666 pofile, potmsgset = self._makePOFileAndPOTMsgSet()
1667 translations = self._makeTranslations(potmsgset)
1668
1669 first_message = potmsgset.setCurrentTranslation(
1670 pofile, pofile.potemplate.owner, translations,
1671 RosettaTranslationOrigin.ROSETTAWEB, TranslationSide.UPSTREAM)
1672 second_message = potmsgset.setCurrentTranslation(
1673 pofile, self.factory.makePerson(), translations,
1674 RosettaTranslationOrigin.SCM, TranslationSide.UPSTREAM)
1675
1676 self.assertEqual(first_message, second_message)
1677 message = first_message
1678 self.assertEqual(pofile.potemplate.owner, message.submitter)
1679 self.assertEqual(RosettaTranslationOrigin.ROSETTAWEB, message.origin)
1680
1681 def test_upstream_also_sets_initial_ubuntu(self):
1682 # Setting an upstream translation also initializes the Ubuntu
1683 # translation.
1684 pofile, potmsgset = self._makePOFileAndPOTMsgSet()
1685 translations = self._makeTranslations(potmsgset)
1686 origin = RosettaTranslationOrigin.ROSETTAWEB
1687
1688 message = potmsgset.setCurrentTranslation(
1689 pofile, pofile.potemplate.owner, translations, origin,
1690 TranslationSide.UPSTREAM)
1691
1692 self.assertEqual(message, potmsgset.getImportedTranslationMessage(
1693 pofile.potemplate, pofile.language))
1694
1695
1575def test_suite():1696def test_suite():
1576 return unittest.TestLoader().loadTestsFromName(__name__)1697 return unittest.TestLoader().loadTestsFromName(__name__)
15771698
=== 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-21 16:42:29 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009 Canonical Ltd. This software is licensed under the1# Copyright 2009-2010 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Unit tests for `TranslationMessage`."""4"""Unit tests for `TranslationMessage`."""
@@ -10,12 +10,48 @@
10from zope.component import getUtility10from zope.component import getUtility
11from zope.security.proxy import removeSecurityProxy11from zope.security.proxy import removeSecurityProxy
1212
13from canonical.launchpad.webapp.testing import verifyObject
13from lp.services.worlddata.interfaces.language import ILanguageSet14from lp.services.worlddata.interfaces.language import ILanguageSet
14from lp.testing import TestCaseWithFactory15from lp.testing import TestCaseWithFactory
15from lp.translations.model.potranslation import POTranslation16from lp.translations.model.potranslation import POTranslation
17from lp.translations.model.translationmessage import DummyTranslationMessage
18from lp.translations.interfaces.translationmessage import (
19 ITranslationMessage)
16from lp.translations.interfaces.translations import TranslationConstants20from lp.translations.interfaces.translations import TranslationConstants
17from canonical.testing import ZopelessDatabaseLayer21from canonical.testing import ZopelessDatabaseLayer
1822
23
24class TestTranslationMessage(TestCaseWithFactory):
25 """Unit tests for `TranslationMessage`.
26
27 There aren't many of these. We didn't do much unit testing back
28 then.
29 """
30
31 layer = ZopelessDatabaseLayer
32
33 def test_baseline(self):
34 message = self.factory.makeTranslationMessage()
35 verifyObject(ITranslationMessage, message)
36
37 def test_dummy_translationmessage(self):
38 pofile = self.factory.makePOFile('nl')
39 potmsgset = self.factory.makePOTMsgSet(pofile.potemplate)
40 dummy = DummyTranslationMessage(pofile, potmsgset)
41 verifyObject(ITranslationMessage, dummy)
42
43 def test_is_diverged_false(self):
44 # ITranslationMessage.is_diverged is a little helper to let you
45 # say "message.is_diverged" which can be clearer than
46 # "message.potemplate is not None."
47 message = self.factory.makeTranslationMessage(force_diverged=False)
48 self.assertFalse(message.is_diverged)
49
50 def test_is_diverged_true(self):
51 message = self.factory.makeTranslationMessage(force_diverged=True)
52 self.assertTrue(message.is_diverged)
53
54
19class TestTranslationMessageFindIdenticalMessage(TestCaseWithFactory):55class TestTranslationMessageFindIdenticalMessage(TestCaseWithFactory):
20 """Tests for `TranslationMessage.findIdenticalMessage`."""56 """Tests for `TranslationMessage.findIdenticalMessage`."""
2157

Subscribers

People subscribed via source and target branches