Merge lp:~henninge/launchpad/recife-bug-611674-convert-imports into lp:~launchpad/launchpad/recife

Proposed by Henning Eggers
Status: Merged
Approved by: Данило Шеган
Approved revision: no longer in the source branch.
Merged at revision: 9172
Proposed branch: lp:~henninge/launchpad/recife-bug-611674-convert-imports
Merge into: lp:~launchpad/launchpad/recife
Diff against target: 595 lines (+321/-83)
8 files modified
lib/lp/translations/doc/poimport-script.txt (+2/-2)
lib/lp/translations/doc/rosetta-karma.txt (+5/-4)
lib/lp/translations/model/pofile.py (+3/-5)
lib/lp/translations/stories/translations/30-rosetta-pofile-translation-gettext-error.txt (+12/-20)
lib/lp/translations/utilities/sanitize.py (+2/-0)
lib/lp/translations/utilities/tests/test_file_importer.py (+151/-3)
lib/lp/translations/utilities/tests/test_sanitize.py (+14/-0)
lib/lp/translations/utilities/translation_import.py (+132/-49)
To merge this branch: bzr merge lp:~henninge/launchpad/recife-bug-611674-convert-imports
Reviewer Review Type Date Requested Status
Данило Шеган (community) Approve
Review via email: mp+37108@code.launchpad.net

Description of the change

Bug 611674
==========

Although the bug does not say so, this branch is about converting the import code to the new models and use the new methods for adding translations and marking them as current.

The main change is in FileImporter.storeTranslations which used to call updateTranslations. The new scheme is to
  1. Add the new translation as a suggestion
  2. Try to validate it
  3. Approve the suggestion if it validates and has no conflicts.

A large part of the diff is made up of the new property "FileImporter.share_with_other_side" and its tests. The property is a boolean that reflects the sharing policy for translations from the file that is being imported. The policy depends on where the translations are coming from and what they are being imported into and on the permissions of the uploader. This was isolated to be able to change this policy later. (And I admit it could have warranted an extra branch.)

Implementation details
----------------------

rosetta-karma.txt:
Because of the new scheme, import karma is now awarded for adding suggestions, not for approving them. Slightly different but not much.

30-rosetta-pofile-translation-gettext-error.txt:
I don't know why gettextpo mentions "msgid_plural" nowadays. This code will pass for both "msgid" and "msgid_plural".

sanitize.py, test_sanitize.py:
A test failed because a language returned "None" for pluralforms. I am not sure if that is a bug in itself.

translation_import.py:
The new methods translations_are_msgids, _storeCredits, _validateMessage and _approveMessage are all code that used to be in storeTranslationsInDatabase. This should make it more readable (based on a suggestion by jtv).
The flow in storeStoreTranslationsInDatabase has changed slightly but as all tests are passing ...

Tests
-----

I ran all lp.translations tests to find breakage but I think something like this would cover most of the import code:

bin/test -vvcm lp.translations -t poimport.txt -t poimport-script.txt -t test_file_importer -t test_sanitize -t stories.translations

Lint
----

Lint complains about bad indention in 30-rosetta-pofile-translation-gettext-error.txt which is true but which I am not going to fix here.

To post a comment you must log in.
Revision history for this message
Данило Шеган (danilo) wrote :
Download full text (19.0 KiB)

Hi Henning,

Thanks for tackling this. You've done a part of another card with this
job, so good job there :)

I feel it still needs a few improvements. See below.

  review needs-fixing

> A large part of the diff is made up of the new property
> "FileImporter.share_with_other_side" and its tests. The property is a
> boolean that reflects the sharing policy for translations from the
> file that is being imported. The policy depends on where the
> translations are coming from and what they are being imported into and
> on the permissions of the uploader. This was isolated to be able to
> change this policy later. (And I admit it could have warranted an
> extra branch.)

Interesting approach. We'll need to extract this even further, but
we've already got a separate card for that. You've just made that job
simpler (though, it's not going to be identical, since we'll have to
support the specifics of the web UI as well).

> === modified file 'lib/lp/translations/doc/poimport-script.txt'
> --- lib/lp/translations/doc/poimport-script.txt 2010-09-21 16:19:47
> +0000
> +++ lib/lp/translations/doc/poimport-script.txt 2010-09-30 08:53:45 +0000
> @@ -282,9 +282,9 @@
>
> >>> from lp.services.worlddata.interfaces.language import ILanguageSet
> >>> esperanto = getUtility(ILanguageSet).getLanguageByCode('eo')
> - >>> foos = potemplate['Foo %s'].getLocalTranslationMessages(
> + >>> suggestions = potemplate['Foo %
> s'].getLocalTranslationMessages(
> ... potemplate, esperanto)
> - >>> sorted([foo.msgstr0.translation for foo in foos])
> + >>> sorted([sugg.msgstr0.translation for sugg in suggestions])
> [u'Bar', u'Bars']

I don't really like "sugg" much better than "foo" (though, "suggestions"
instead of "foos" IS MUCH better :). How about "suggestion"?

> === modified file 'lib/lp/translations/utilities/sanitize.py'
> --- lib/lp/translations/utilities/sanitize.py 2010-08-16 09:01:35 +0000
> +++ lib/lp/translations/utilities/sanitize.py 2010-09-30 08:53:45 +0000
> @@ -181,6 +181,8 @@
>
> # Expected plural forms should all exist and empty translations should
> # be normalized to None.
> + if pluralforms is None:
> + pluralforms = 2
> for pluralform in range(pluralforms):
> if pluralform not in sanitized_translations:
> sanitized_translations[pluralform] = None

What's the reasoning for this? Why not default to 1 instead (i.e.
assume it's a message with no plurals at all)?

> === modified file 'lib/lp/translations/utilities/tests/test_sanitize.py'
> --- lib/lp/translations/utilities/tests/test_sanitize.py 2010-08-23 08:41:03 +0000
> +++ lib/lp/translations/utilities/tests/test_sanitize.py 2010-09-30 08:53:45 +0000
> @@ -257,6 +257,20 @@
> expected_sanitized,
> sanitize_translations_from_webui(self.english, translations, 3))
>
> + def test_sanitize_translations_pluralforms_none(self):
> + # Some languages don't provide a plural form, so 2 is assumed.
> + translations = {
> + 0: u'Plural form 0 ',
> + }
> + expected_sanitized = {
> + 0: u'Plural form 0',
> + ...

review: Needs Fixing
Revision history for this message
Henning Eggers (henninge) wrote :
Download full text (17.1 KiB)

> > === modified file 'lib/lp/translations/doc/poimport-script.txt'
> > --- lib/lp/translations/doc/poimport-script.txt 2010-09-21 16:19:47
> > +0000
> > +++ lib/lp/translations/doc/poimport-script.txt 2010-09-30 08:53:45 +0000
> > @@ -282,9 +282,9 @@
> >
> > >>> from lp.services.worlddata.interfaces.language import ILanguageSet
> > >>> esperanto = getUtility(ILanguageSet).getLanguageByCode('eo')
> > - >>> foos = potemplate['Foo %s'].getLocalTranslationMessages(
> > + >>> suggestions = potemplate['Foo %
> > s'].getLocalTranslationMessages(
> > ... potemplate, esperanto)
> > - >>> sorted([foo.msgstr0.translation for foo in foos])
> > + >>> sorted([sugg.msgstr0.translation for sugg in suggestions])
> > [u'Bar', u'Bars']
>
> I don't really like "sugg" much better than "foo" (though, "suggestions"
> instead of "foos" IS MUCH better :). How about "suggestion"?

Yeah, I didn't think it would fit. It does.

>
> > === modified file 'lib/lp/translations/utilities/sanitize.py'
> > --- lib/lp/translations/utilities/sanitize.py 2010-08-16 09:01:35 +0000
> > +++ lib/lp/translations/utilities/sanitize.py 2010-09-30 08:53:45 +0000
> > @@ -181,6 +181,8 @@
> >
> > # Expected plural forms should all exist and empty translations should
> > # be normalized to None.
> > + if pluralforms is None:
> > + pluralforms = 2
> > for pluralform in range(pluralforms):
> > if pluralform not in sanitized_translations:
> > sanitized_translations[pluralform] = None
>
> What's the reasoning for this? Why not default to 1 instead (i.e.
> assume it's a message with no plurals at all)?

That's what Language does. I could remove it here and use language.guessed_pluralforms when calling the function.
Also, there is something else I was wondering about. The function will fill in the missing plural forms with None, regardless of whether there is a msgid_plural or not. I don't remember if that is intended behavior. It does no harm but seems like a waste.

>
> > === modified file
> 'lib/lp/translations/utilities/tests/test_file_importer.py'
> ...
> > +class FileImporterSharingTest(TestCaseWithFactory):
> > + """Class test for the sharing operation of the FileImporter base
> class."""
> > + layer = LaunchpadZopelessLayer
> > +
> > + UPSTREAM = 0
> > + UBUNTU = 1
> > + LANGCODE = 'eo'
>
> I know this is how we usually used to do stuff, but today we should be
> able to also make languages and not depend on existing ones in the
> sampledata. For instance, if you really want to keep using 'eo', I'd
> suggest you do factory.makeLanguage('eo@test').

No, I am not intend on this. Actually I defined it here so that it does not appear in the code itself. Changed it to use a constructed Language.

>
> > + POFILE = dedent("""\
> > + msgid ""
> > + msgstr ""
> > + "PO-Revision-Date: 2005-05-03 20:41+0100\\n"
> > + "Last-Translator: FULL NAME <EMAIL@ADDRESS>\\n"
> > + "Content-Type: text/plain; charset=UTF-8\\n"
> > + "X-Launchpad-Export-Date: 2009-05-14 08:54+0000\\n"
> > +
> > + msgid "Thank You"
> > + msgstr "Dankon"
> > + ...

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

Incremental diff.

=== modified file 'lib/lp/translations/doc/poimport-script.txt'
--- lib/lp/translations/doc/poimport-script.txt 2010-09-29 11:53:45 +0000
+++ lib/lp/translations/doc/poimport-script.txt 2010-10-01 10:15:02 +0000
@@ -284,7 +284,7 @@
284 >>> esperanto = getUtility(ILanguageSet).getLanguageByCode('eo')284 >>> esperanto = getUtility(ILanguageSet).getLanguageByCode('eo')
285 >>> suggestions = potemplate['Foo %s'].getLocalTranslationMessages(285 >>> suggestions = potemplate['Foo %s'].getLocalTranslationMessages(
286 ... potemplate, esperanto)286 ... potemplate, esperanto)
287 >>> sorted([sugg.msgstr0.translation for sugg in suggestions])287 >>> sorted([suggestion.msgstr0.translation for suggestion in suggestions])
288 [u'Bar', u'Bars']288 [u'Bar', u'Bars']
289289
290Since this last upload was not the upstream one, however, its credits290Since this last upload was not the upstream one, however, its credits
291291
=== modified file 'lib/lp/translations/utilities/tests/test_file_importer.py'
--- lib/lp/translations/utilities/tests/test_file_importer.py 2010-09-30 08:51:21 +0000
+++ lib/lp/translations/utilities/tests/test_file_importer.py 2010-10-01 13:56:41 +0000
@@ -582,7 +582,6 @@
582582
583 UPSTREAM = 0583 UPSTREAM = 0
584 UBUNTU = 1584 UBUNTU = 1
585 LANGCODE = 'eo'
586585
587 POFILE = dedent("""\586 POFILE = dedent("""\
588 msgid ""587 msgid ""
@@ -593,13 +592,14 @@
593 "X-Launchpad-Export-Date: 2009-05-14 08:54+0000\\n"592 "X-Launchpad-Export-Date: 2009-05-14 08:54+0000\\n"
594593
595 msgid "Thank You"594 msgid "Thank You"
596 msgstr "Dankon"595 msgstr "Translation"
597 """)596 """)
598597
599 def setUp(self):598 def setUp(self):
600 super(FileImporterSharingTest, self).setUp()599 super(FileImporterSharingTest, self).setUp()
601 # Create the upstream series and template with a translator.600 # Create the upstream series and template with a translator.
602 self.translator = self.factory.makeTranslator(self.LANGCODE)601 self.language = self.factory.makeLanguage()
602 self.translator = self.factory.makeTranslator(self.language.code)
603 self.upstream_productseries = self.factory.makeProductSeries()603 self.upstream_productseries = self.factory.makeProductSeries()
604 self.upstream_productseries.product.translationgroup = (604 self.upstream_productseries.product.translationgroup = (
605 self.translator.translationgroup)605 self.translator.translationgroup)
@@ -608,12 +608,12 @@
608 self.upstream_template = self.factory.makePOTemplate(608 self.upstream_template = self.factory.makePOTemplate(
609 productseries=self.upstream_productseries)609 productseries=self.upstream_productseries)
610610
611 def _makeEntry(self, side, from_upstream=False, uploader=None):611 def _makeImportEntry(self, side, from_upstream=False, uploader=None,
612 no_upstream=False):
612 if side == self.UPSTREAM:613 if side == self.UPSTREAM:
613 potemplate = self.upstream_template614 potemplate = self.upstream_template
614 else:615 else:
615 # Create a template in a source package and link the source616 # Create a template in a source package.
616 # package to the upstream series to enable sharing.
617 ubuntu = getUtility(ILaunchpadCelebrities).ubuntu617 ubuntu = getUtility(ILaunchpadCelebrities).ubuntu
618 distroseries = self.factory.makeDistroSeries(distribution=ubuntu)618 distroseries = self.factory.makeDistroSeries(distribution=ubuntu)
619 ubuntu.translation_focus = distroseries619 ubuntu.translation_focus = distroseries
@@ -622,19 +622,24 @@
622 distroseries=distroseries,622 distroseries=distroseries,
623 sourcepackagename=sourcepackagename,623 sourcepackagename=sourcepackagename,
624 name=self.upstream_template.name)624 name=self.upstream_template.name)
625 self.factory.makeSourcePackagePublishingHistory(625 if not no_upstream:
626 sourcepackagename=sourcepackagename,626 # Link the source package to the upstream series to
627 distroseries=distroseries)627 # enable sharing.
628 sourcepackage = distroseries.getSourcePackage(sourcepackagename)628 self.factory.makeSourcePackagePublishingHistory(
629 sourcepackage.setPackaging(629 sourcepackagename=sourcepackagename,
630 self.upstream_productseries, self.factory.makePerson())630 distroseries=distroseries)
631 sourcepackage = distroseries.getSourcePackage(
632 sourcepackagename)
633 sourcepackage.setPackaging(
634 self.upstream_productseries, self.factory.makePerson())
631 pofile = self.factory.makePOFile(635 pofile = self.factory.makePOFile(
632 self.LANGCODE, potemplate=potemplate, create_sharing=True)636 self.language.code, potemplate=potemplate, create_sharing=True)
633 entry = self.factory.makeTranslationImportQueueEntry(637 entry = self.factory.makeTranslationImportQueueEntry(
634 potemplate=potemplate, from_upstream=from_upstream,638 potemplate=potemplate, from_upstream=from_upstream,
635 uploader=uploader, content=self.POFILE)639 uploader=uploader, content=self.POFILE)
636 entry.potemplate = potemplate640 entry.potemplate = potemplate
637 entry.pofile = pofile641 entry.pofile = pofile
642 # The uploaded file is only created in the librarian by a commit.
638 transaction.commit()643 transaction.commit()
639 return entry644 return entry
640645
@@ -642,7 +647,7 @@
642 # Sanity check that the translator has the right permissions but647 # Sanity check that the translator has the right permissions but
643 # others don't.648 # others don't.
644 pofile = self.factory.makePOFile(649 pofile = self.factory.makePOFile(
645 self.LANGCODE, potemplate=self.upstream_template)650 self.language.code, potemplate=self.upstream_template)
646 self.assertFalse(651 self.assertFalse(
647 pofile.canEditTranslations(self.factory.makePerson()))652 pofile.canEditTranslations(self.factory.makePerson()))
648 self.assertTrue(653 self.assertTrue(
@@ -650,7 +655,7 @@
650655
651 def test_templates_are_sharing(self):656 def test_templates_are_sharing(self):
652 # Sharing between upstream and Ubuntu was set up correctly.657 # Sharing between upstream and Ubuntu was set up correctly.
653 entry = self._makeEntry(self.UBUNTU)658 entry = self._makeImportEntry(self.UBUNTU)
654 subset = getUtility(IPOTemplateSet).getSharingSubset(659 subset = getUtility(IPOTemplateSet).getSharingSubset(
655 distribution=entry.distroseries.distribution,660 distribution=entry.distroseries.distribution,
656 sourcepackagename=entry.sourcepackagename)661 sourcepackagename=entry.sourcepackagename)
@@ -660,7 +665,7 @@
660665
661 def test_share_with_other_side_upstream(self):666 def test_share_with_other_side_upstream(self):
662 # An upstream queue entry will be shared with ubuntu.667 # An upstream queue entry will be shared with ubuntu.
663 entry = self._makeEntry(self.UPSTREAM)668 entry = self._makeImportEntry(self.UPSTREAM)
664 importer = POFileImporter(669 importer = POFileImporter(
665 entry, importers[TranslationFileFormat.PO], None)670 entry, importers[TranslationFileFormat.PO], None)
666 self.assertTrue(671 self.assertTrue(
@@ -669,7 +674,16 @@
669674
670 def test_share_with_other_side_ubuntu(self):675 def test_share_with_other_side_ubuntu(self):
671 # An ubuntu queue entry will not be shared with upstream.676 # An ubuntu queue entry will not be shared with upstream.
672 entry = self._makeEntry(self.UBUNTU)677 entry = self._makeImportEntry(self.UBUNTU)
678 importer = POFileImporter(
679 entry, importers[TranslationFileFormat.PO], None)
680 self.assertFalse(
681 importer.share_with_other_side,
682 "Ubuntu import should not share with upstream.")
683
684 def test_share_with_other_side_ubuntu_no_upstream(self):
685 # An ubuntu queue entry cannot share with a non-existent upstream.
686 entry = self._makeImportEntry(self.UBUNTU, no_upstream=True)
673 importer = POFileImporter(687 importer = POFileImporter(
674 entry, importers[TranslationFileFormat.PO], None)688 entry, importers[TranslationFileFormat.PO], None)
675 self.assertFalse(689 self.assertFalse(
@@ -679,7 +693,7 @@
679 def test_share_with_other_side_ubuntu_from_package(self):693 def test_share_with_other_side_ubuntu_from_package(self):
680 # An ubuntu queue entry that is imported from an upstream package694 # An ubuntu queue entry that is imported from an upstream package
681 # will be shared with upstream.695 # will be shared with upstream.
682 entry = self._makeEntry(self.UBUNTU, from_upstream=True)696 entry = self._makeImportEntry(self.UBUNTU, from_upstream=True)
683 importer = POFileImporter(697 importer = POFileImporter(
684 entry, importers[TranslationFileFormat.PO], None)698 entry, importers[TranslationFileFormat.PO], None)
685 self.assertTrue(699 self.assertTrue(
@@ -689,7 +703,7 @@
689 def test_share_with_other_side_ubuntu_uploader_upstream_translator(self):703 def test_share_with_other_side_ubuntu_uploader_upstream_translator(self):
690 # If the uploader in ubuntu has rights on upstream as well, the704 # If the uploader in ubuntu has rights on upstream as well, the
691 # translations are shared.705 # translations are shared.
692 entry = self._makeEntry(706 entry = self._makeImportEntry(
693 self.UBUNTU, uploader=self.translator.translator)707 self.UBUNTU, uploader=self.translator.translator)
694 importer = POFileImporter(708 importer = POFileImporter(
695 entry, importers[TranslationFileFormat.PO], None)709 entry, importers[TranslationFileFormat.PO], None)
696710
=== modified file 'lib/lp/translations/utilities/translation_import.py'
--- lib/lp/translations/utilities/translation_import.py 2010-09-30 08:51:21 +0000
+++ lib/lp/translations/utilities/translation_import.py 2010-10-01 14:27:37 +0000
@@ -490,7 +490,11 @@
490490
491 @cachedproperty491 @cachedproperty
492 def translations_are_msgids(self):492 def translations_are_msgids(self):
493 """Are symbolic msgids being used and these are the real ones?"""493 """Are these English strings instead of translations?
494
495 If this template uses symbolic message ids, the English POFile
496 will contain the English original texts that correspond to the
497 symbols."""
494 return (498 return (
495 self.importer.uses_source_string_msgids and499 self.importer.uses_source_string_msgids and
496 self.pofile.language.code == 'en')500 self.pofile.language.code == 'en')
@@ -559,7 +563,6 @@
559 return None563 return None
560564
561 no_translations = (565 no_translations = (
562 message_data.translations == [] or
563 message_data.translations is None or566 message_data.translations is None or
564 not any(message_data.translations))567 not any(message_data.translations))
565 if no_translations:568 if no_translations:
Revision history for this message
Данило Шеган (danilo) wrote :
Download full text (7.3 KiB)

У пет, 01. 10 2010. у 14:43 +0000, Henning Eggers пише:

> That's what Language does. I could remove it here and use
> language.guessed_pluralforms when calling the function. Also, there is
> something else I was wondering about. The function will fill in the
> missing plural forms with None, regardless of whether there is a
> msgid_plural or not. I don't remember if that is intended behavior. It
> does no harm but seems like a waste.

Let's keep it simple for now and not touch anything. It definitely
sounds a bit wasteful, but it's not a big deal.

> > I know this is how we usually used to do stuff, but today we should be
> > able to also make languages and not depend on existing ones in the
> > sampledata. For instance, if you really want to keep using 'eo', I'd
> > suggest you do factory.makeLanguage('eo@test').
>
> No, I am not intend on this. Actually I defined it here so that it does
> not appear in the code itself. Changed it to use a constructed
> Language.

Cool, thanks.

> > > + if side == self.UPSTREAM:
> > > + potemplate = self.upstream_template
> > > + else:
> > > + # Create a template in a source package and link the source
> > > + # package to the upstream series to enable sharing.
> > > + ubuntu = getUtility(ILaunchpadCelebrities).ubuntu
> > > + distroseries =
> > self.factory.makeDistroSeries(distribution=ubuntu)
> > > + ubuntu.translation_focus = distroseries
> > > + sourcepackagename = self.factory.makeSourcePackageName()
> > > + potemplate = self.factory.makePOTemplate(
> > > + distroseries=distroseries,
> > > + sourcepackagename=sourcepackagename,
> > > + name=self.upstream_template.name)
> > > + self.factory.makeSourcePackagePublishingHistory(
> > > + sourcepackagename=sourcepackagename,
> > > + distroseries=distroseries)
> >
> > Why is this necessary? Is it not possible to reach a sourcepackage
> > without it?
>
> For some reason I cannot use sourcepackage.productseries if that has not been set.

Right, it's probably useful knowledge worth sharing across the
translations team at least. Knowing the background would be even better
because then we could reconstruct it in the future :)

> >
> > > + return entry
> > > +
> > > + def test_translator_persmissions(self):
> >
> > s/perSmissions/permissions/

You seem to have forgotten this bit.

> >
> > > + def test_templates_are_sharing(self):
> > > + # Sharing between upstream and Ubuntu was set up correctly.
> > > + entry = self._makeEntry(self.UBUNTU)
> > > + subset = getUtility(IPOTemplateSet).getSharingSubset(
> > > + distribution=entry.distroseries.distribution,
> > > + sourcepackagename=entry.sourcepackagename)
> > > + self.assertContentEqual(
> > > + [entry.potemplate, self.upstream_template],
> > > + list(subset.getSharingPOTemplates(entry.potemplate.name)))
> >
> > This looks a lot like a test for the code that's not really here,
> > doesn't it? :)
> >
> > Or, is it basicall...

Read more...

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

Am 05.10.2010 13:34, schrieb Данило Шеган:
>>>> + self.factory.makeSourcePackagePublishingHistory(
>>>> + sourcepackagename=sourcepackagename,
>>>> + distroseries=distroseries)
>>>
>>> Why is this necessary? Is it not possible to reach a sourcepackage
>>> without it?
>>
>> For some reason I cannot use sourcepackage.productseries if that has not been set.
>
> Right, it's probably useful knowledge worth sharing across the
> translations team at least. Knowing the background would be even better
> because then we could reconstruct it in the future :)

Hm, should I put a card in the backlog to research that?

>
>>>
>>>> + return entry
>>>> +
>>>> + def test_translator_persmissions(self):
>>>
>>> s/perSmissions/permissions/
>
> You seem to have forgotten this bit.

I did, thanks. ;)

> It'd still be nice to name the test so it reflects this: eg.
> "test_makeImportEntry_templates_are_sharing".

Renamed.

>
>>> Cool stuff. It's amazing how tests can be so simple for something this
>>> intricate. :)
>>
>> I see the smiley but I am not sure if you are making fun of me or not ... ;-/
>
> No, it's a smiley of happiness. Also a credit to your "factor out stuff
> that is not being tested" (your _makeImportEntry method).

Thank you. :-)

>>>> + @cachedproperty
>>>> + def translations_are_msgids(self):
>>>> + """Are symbolic msgids being used and these are the real ones?"""
>>>
>>> This sentence is a bit confusing. If I had no idea about what this is
>>> about, I'd have even less idea about it. :)
>>
>> OK, I tried better. ;-)
>
> I think "text" is a non-countable noun, so plural "texts" sounds weird:

It can be both. In this case it is countable

>>> This is actually very interesting, and I am happy to see that you
>>> extracted it out. It actually points out that the naming for
>>> "from_upstream" is not really perfect: it should probably be
>>> "from_maintainer" or "from_trusted" or something.
>>
>> Yes, Jeroen and I had talked about that, too. I think I like from_maintainer most.
>
> Let's add a card for that then.

Done.

>>> Oh, I just read up on any() :) It seems you can drop the comparison to
>>> the empty list as well.
>>
>> Good catch! I was a bit worried about using "any" because it tests for
>> "False" but we are looking for empty strings and "None". Although
>> these are by definition "False", too, our coding standards always
>> forbade implicit bool checks like these.
>
> That was kind of my worry as well, but I guess there's no harm since the
> intent is clear enough.

I put it on the reviewer meeting agenda.

>
>>> With the new model, there will be no way to know if a translation
>>> credits message didn't validate, but is the actual one from upstream.
>>> Something to think about.
>>
>> Hm, we could still validate and mark the message accordingly, even if
>> it is used, right? I am not sure, though, if translators expect us to
>> catch bugs like that. Maybe we should discard c-format flags on
>> translation credits POTMsgSets altogether?
>
> Definitely worth thinking about some more. How about you file a bug
> about this to look into after all the "re...

Read more...

=== modified file 'lib/lp/translations/utilities/tests/test_file_importer.py'
--- lib/lp/translations/utilities/tests/test_file_importer.py 2010-10-05 17:06:23 +0000
+++ lib/lp/translations/utilities/tests/test_file_importer.py 2010-10-06 09:40:23 +0000
@@ -643,7 +643,7 @@
643 transaction.commit()643 transaction.commit()
644 return entry644 return entry
645645
646 def test_translator_persmissions(self):646 def test_translator_permissions(self):
647 # Sanity check that the translator has the right permissions but647 # Sanity check that the translator has the right permissions but
648 # others don't.648 # others don't.
649 pofile = self.factory.makePOFile(649 pofile = self.factory.makePOFile(
@@ -653,7 +653,7 @@
653 self.assertTrue(653 self.assertTrue(
654 pofile.canEditTranslations(self.translator.translator))654 pofile.canEditTranslations(self.translator.translator))
655655
656 def test_templates_are_sharing(self):656 def test_makeImportEntry_templates_are_sharing(self):
657 # Sharing between upstream and Ubuntu was set up correctly.657 # Sharing between upstream and Ubuntu was set up correctly.
658 entry = self._makeImportEntry(self.UBUNTU)658 entry = self._makeImportEntry(self.UBUNTU)
659 subset = getUtility(IPOTemplateSet).getSharingSubset(659 subset = getUtility(IPOTemplateSet).getSharingSubset(

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/translations/doc/poimport-script.txt'
--- lib/lp/translations/doc/poimport-script.txt 2010-09-21 16:19:47 +0000
+++ lib/lp/translations/doc/poimport-script.txt 2010-10-06 11:07:49 +0000
@@ -282,9 +282,9 @@
282282
283 >>> from lp.services.worlddata.interfaces.language import ILanguageSet283 >>> from lp.services.worlddata.interfaces.language import ILanguageSet
284 >>> esperanto = getUtility(ILanguageSet).getLanguageByCode('eo')284 >>> esperanto = getUtility(ILanguageSet).getLanguageByCode('eo')
285 >>> foos = potemplate['Foo %s'].getLocalTranslationMessages(285 >>> suggestions = potemplate['Foo %s'].getLocalTranslationMessages(
286 ... potemplate, esperanto)286 ... potemplate, esperanto)
287 >>> sorted([foo.msgstr0.translation for foo in foos])287 >>> sorted([suggestion.msgstr0.translation for suggestion in suggestions])
288 [u'Bar', u'Bars']288 [u'Bar', u'Bars']
289289
290Since this last upload was not the upstream one, however, its credits290Since this last upload was not the upstream one, however, its credits
291291
=== modified file 'lib/lp/translations/doc/rosetta-karma.txt'
--- lib/lp/translations/doc/rosetta-karma.txt 2010-08-04 11:00:51 +0000
+++ lib/lp/translations/doc/rosetta-karma.txt 2010-10-06 11:07:49 +0000
@@ -221,14 +221,14 @@
221Tell the PO file to import from the file data it has. The user has rights221Tell the PO file to import from the file data it has. The user has rights
222to edit translations directly, so his suggestion is approved directly.222to edit translations directly, so his suggestion is approved directly.
223Even when the user is also a reviwer, he should not get karma for reviewing223Even when the user is also a reviwer, he should not get karma for reviewing
224his own translations. IOW, he'll only get karma for his suggestion being224or approving his own translations. IOW, he'll only get karma for submitting
225approved.225his suggestion.
226226
227 >>> potemplate = POTemplate.get(1)227 >>> potemplate = POTemplate.get(1)
228 >>> entry = translation_import_queue[entry_id]228 >>> entry = translation_import_queue[entry_id]
229 >>> pofile = potemplate.getPOFileByLang('es')229 >>> pofile = potemplate.getPOFileByLang('es')
230 >>> (subject, message) = pofile.importFromQueue(entry)230 >>> (subject, message) = pofile.importFromQueue(entry)
231 Karma added: action=translationsuggestionapproved, product=evolution231 Karma added: action=translationsuggestionadded, product=evolution
232 >>> transaction.commit()232 >>> transaction.commit()
233233
234Let's try the case when a file is uploaded, but no translation is changed.234Let's try the case when a file is uploaded, but no translation is changed.
@@ -384,7 +384,8 @@
384We do the update and see the karma being assigned.384We do the update and see the karma being assigned.
385385
386 >>> status = potemplate_view.initialize()386 >>> status = potemplate_view.initialize()
387 Karma added: action=translationtemplatedescriptionchanged, product=evolution387 Karma added:
388 action=translationtemplatedescriptionchanged, product=evolution
388389
389And the new one is:390And the new one is:
390391
391392
=== modified file 'lib/lp/translations/model/pofile.py'
--- lib/lp/translations/model/pofile.py 2010-09-06 10:40:54 +0000
+++ lib/lp/translations/model/pofile.py 2010-10-06 11:07:49 +0000
@@ -538,7 +538,8 @@
538 applicable_template = Coalesce(538 applicable_template = Coalesce(
539 TranslationMessage.potemplateID, self.potemplate.id)539 TranslationMessage.potemplateID, self.potemplate.id)
540 clauses = [540 clauses = [
541 TranslationTemplateItem.potmsgsetID == TranslationMessage.potmsgsetID,541 TranslationTemplateItem.potmsgsetID == (
542 TranslationMessage.potmsgsetID),
542 TranslationTemplateItem.potemplate == self.potemplate,543 TranslationTemplateItem.potemplate == self.potemplate,
543 TranslationMessage.language == self.language,544 TranslationMessage.language == self.language,
544 applicable_template == self.potemplate.id,545 applicable_template == self.potemplate.id,
@@ -1108,8 +1109,7 @@
11081109
1109 # Prepare the mail notification.1110 # Prepare the mail notification.
1110 msgsets_imported = self.getTranslationMessages(1111 msgsets_imported = self.getTranslationMessages(
1111 TranslationMessage.was_obsolete_in_last_import == False1112 TranslationMessage.was_obsolete_in_last_import == False).count()
1112 ).count()
11131113
1114 replacements = collect_import_info(entry_to_import, self, warnings)1114 replacements = collect_import_info(entry_to_import, self, warnings)
1115 replacements.update({1115 replacements.update({
@@ -1179,8 +1179,6 @@
1179 # Assign karma to the importer if this is not an automatic import1179 # Assign karma to the importer if this is not an automatic import
1180 # (all automatic imports come from the rosetta expert user) and1180 # (all automatic imports come from the rosetta expert user) and
1181 # comes from upstream.1181 # comes from upstream.
1182 celebs = getUtility(ILaunchpadCelebrities)
1183 rosetta_experts = celebs.rosetta_experts
1184 if (entry_to_import.from_upstream and1182 if (entry_to_import.from_upstream and
1185 entry_to_import.importer.id != rosetta_experts.id):1183 entry_to_import.importer.id != rosetta_experts.id):
1186 entry_to_import.importer.assignKarma(1184 entry_to_import.importer.assignKarma(
11871185
=== modified file 'lib/lp/translations/stories/translations/30-rosetta-pofile-translation-gettext-error.txt'
--- lib/lp/translations/stories/translations/30-rosetta-pofile-translation-gettext-error.txt 2010-09-20 05:32:10 +0000
+++ lib/lp/translations/stories/translations/30-rosetta-pofile-translation-gettext-error.txt 2010-10-06 11:07:49 +0000
@@ -38,38 +38,30 @@
38 >>> user_browser.getControl(38 >>> user_browser.getControl(
39 ... name='msgset_150_es_translation_1_radiobutton').value = [39 ... name='msgset_150_es_translation_1_radiobutton').value = [
40 ... 'msgset_150_es_translation_1_new']40 ... 'msgset_150_es_translation_1_new']
41 >>> user_browser.getControl(name='msgset_150_es_translation_1_new').value = (41 >>> user_browser.getControl(
42 ... name='msgset_150_es_translation_1_new').value = (
42 ... u'Found %s invalid files')43 ... u'Found %s invalid files')
43 >>> user_browser.getControl(name='submit_translations').click()44 >>> user_browser.getControl(name='submit_translations').click()
4445
45Because of the error, we're still in on the same page.46Because of the error, we're still in on the same page.
4647
47 >>> print user_browser.url48 >>> print user_browser.url
48 http://translations.launchpad.dev/ubuntu/hoary/+source/evolution/+pots/evolution-2.2/es/+translate?start=2049 http://.../hoary/+source/evo.../+pots/evo...-2.2/es/+translate?start=20
4950
50And we can see the error.51And we can see the error.
5152
52 >>> for tag in find_tags_by_class(user_browser.contents, 'error'):53 >>> for tag in find_tags_by_class(user_browser.contents, 'error'):
53 ... print tag54 ... print extract_text(tag)
54 <div class="error message">There is an error in a translation you provided.55 There is an error in a translation you provided.
55 Please correct it before continuing.</div>56 Please correct it before continuing.
56 <tr class="error translation">57 Error in Translation:
57 <th colspan="3">58 format specifications in 'msgid...' and 'msgstr[0]' for argument 1 are
58 <strong>Error in Translation:</strong>59 not the sameformat specifications in 'msgid...' and 'msgstr[1]' for
59 </th>60 argument 1 are not the same
60 <td>
61 </td><td>
62 <div>
63 format specifications in ... and 'msgstr[0]' for argument 1 are
64 not the sameformat specifications in ... and 'msgstr[1]' for
65 argument 1 are not the same
66 </div>
67 </td>
68 </tr>
6961
70The translation form got an error with the translations we wanted to store,62The translation form got an error with the translations we wanted to store,
71and thus we still have that text as part of translations input, otherwise, they63and thus we still have that text as part of translations input, otherwise,
72will be empty waiting for new suggestions/translations.64they will be empty waiting for new suggestions/translations.
7365
74 >>> user_browser.getControl(name='msgset_150_es_translation_0_new').value66 >>> user_browser.getControl(name='msgset_150_es_translation_0_new').value
75 'Found %s invalid file'67 'Found %s invalid file'
7668
=== modified file 'lib/lp/translations/utilities/sanitize.py'
--- lib/lp/translations/utilities/sanitize.py 2010-08-16 09:01:35 +0000
+++ lib/lp/translations/utilities/sanitize.py 2010-10-06 11:07:49 +0000
@@ -181,6 +181,8 @@
181181
182 # Expected plural forms should all exist and empty translations should182 # Expected plural forms should all exist and empty translations should
183 # be normalized to None.183 # be normalized to None.
184 if pluralforms is None:
185 pluralforms = 2
184 for pluralform in range(pluralforms):186 for pluralform in range(pluralforms):
185 if pluralform not in sanitized_translations:187 if pluralform not in sanitized_translations:
186 sanitized_translations[pluralform] = None188 sanitized_translations[pluralform] = None
187189
=== modified file 'lib/lp/translations/utilities/tests/test_file_importer.py'
--- lib/lp/translations/utilities/tests/test_file_importer.py 2010-09-30 17:57:01 +0000
+++ lib/lp/translations/utilities/tests/test_file_importer.py 2010-10-06 11:07:49 +0000
@@ -5,13 +5,24 @@
55
6__metaclass__ = type6__metaclass__ = type
77
8from textwrap import dedent
9
10import transaction
8from zope.component import getUtility11from zope.component import getUtility
9from zope.security.proxy import removeSecurityProxy12from zope.security.proxy import removeSecurityProxy
1013
11from canonical.testing import ZopelessDatabaseLayer14from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
15from canonical.testing import (
16 LaunchpadZopelessLayer,
17 ZopelessDatabaseLayer,
18 )
12from lp.registry.interfaces.person import IPersonSet19from lp.registry.interfaces.person import IPersonSet
13from lp.testing import TestCaseWithFactory20from lp.testing import TestCaseWithFactory
14from lp.testing.fakelibrarian import FakeLibrarian21from lp.testing.fakelibrarian import FakeLibrarian
22from lp.translations.interfaces.potemplate import IPOTemplateSet
23from lp.translations.interfaces.translationfileformat import (
24 TranslationFileFormat,
25 )
15from lp.translations.interfaces.translationgroup import TranslationPermission26from lp.translations.interfaces.translationgroup import TranslationPermission
16from lp.translations.interfaces.translationimporter import (27from lp.translations.interfaces.translationimporter import (
17 OutdatedTranslationError,28 OutdatedTranslationError,
@@ -23,6 +34,7 @@
23from lp.translations.utilities.translation_common_format import (34from lp.translations.utilities.translation_common_format import (
24 TranslationMessageData)35 TranslationMessageData)
25from lp.translations.utilities.translation_import import (36from lp.translations.utilities.translation_import import (
37 importers,
26 FileImporter,38 FileImporter,
27 POFileImporter,39 POFileImporter,
28 POTFileImporter,40 POTFileImporter,
@@ -367,11 +379,11 @@
367 def test_FileImporter_importFile_ok(self):379 def test_FileImporter_importFile_ok(self):
368 # Test correct import operation for both380 # Test correct import operation for both
369 # exported and upstream files.381 # exported and upstream files.
370 importers = (382 used_importers = (
371 self._createImporterForExportedEntries(),383 self._createImporterForExportedEntries(),
372 self._createImporterForUpstreamEntries(),384 self._createImporterForUpstreamEntries(),
373 )385 )
374 for (pot_importer, po_importer) in importers:386 for (pot_importer, po_importer) in used_importers:
375 # Run the import and see if PotMsgSet and TranslationMessage387 # Run the import and see if PotMsgSet and TranslationMessage
376 # entries are correctly created in the DB.388 # entries are correctly created in the DB.
377 errors, warnings = pot_importer.importFile()389 errors, warnings = pot_importer.importFile()
@@ -562,3 +574,139 @@
562 old_raw_header = pofile.header574 old_raw_header = pofile.header
563 importer = POFileImporter(queue_entry, GettextPOImporter(), None)575 importer = POFileImporter(queue_entry, GettextPOImporter(), None)
564 self.assertEqual(old_raw_header, pofile.header)576 self.assertEqual(old_raw_header, pofile.header)
577
578
579class FileImporterSharingTest(TestCaseWithFactory):
580 """Class test for the sharing operation of the FileImporter base class."""
581 layer = LaunchpadZopelessLayer
582
583 UPSTREAM = 0
584 UBUNTU = 1
585
586 POFILE = dedent("""\
587 msgid ""
588 msgstr ""
589 "PO-Revision-Date: 2005-05-03 20:41+0100\\n"
590 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\\n"
591 "Content-Type: text/plain; charset=UTF-8\\n"
592 "X-Launchpad-Export-Date: 2009-05-14 08:54+0000\\n"
593
594 msgid "Thank You"
595 msgstr "Translation"
596 """)
597
598 def setUp(self):
599 super(FileImporterSharingTest, self).setUp()
600 # Create the upstream series and template with a translator.
601 self.language = self.factory.makeLanguage()
602 self.translator = self.factory.makeTranslator(self.language.code)
603 self.upstream_productseries = self.factory.makeProductSeries()
604 self.upstream_productseries.product.translationgroup = (
605 self.translator.translationgroup)
606 self.upstream_productseries.product.translationpermission = (
607 TranslationPermission.RESTRICTED)
608 self.upstream_template = self.factory.makePOTemplate(
609 productseries=self.upstream_productseries)
610
611 def _makeImportEntry(self, side, from_upstream=False, uploader=None,
612 no_upstream=False):
613 if side == self.UPSTREAM:
614 potemplate = self.upstream_template
615 else:
616 # Create a template in a source package.
617 ubuntu = getUtility(ILaunchpadCelebrities).ubuntu
618 distroseries = self.factory.makeDistroSeries(distribution=ubuntu)
619 ubuntu.translation_focus = distroseries
620 sourcepackagename = self.factory.makeSourcePackageName()
621 potemplate = self.factory.makePOTemplate(
622 distroseries=distroseries,
623 sourcepackagename=sourcepackagename,
624 name=self.upstream_template.name)
625 if not no_upstream:
626 # Link the source package to the upstream series to
627 # enable sharing.
628 self.factory.makeSourcePackagePublishingHistory(
629 sourcepackagename=sourcepackagename,
630 distroseries=distroseries)
631 sourcepackage = distroseries.getSourcePackage(
632 sourcepackagename)
633 sourcepackage.setPackaging(
634 self.upstream_productseries, self.factory.makePerson())
635 pofile = self.factory.makePOFile(
636 self.language.code, potemplate=potemplate, create_sharing=True)
637 entry = self.factory.makeTranslationImportQueueEntry(
638 potemplate=potemplate, from_upstream=from_upstream,
639 uploader=uploader, content=self.POFILE)
640 entry.potemplate = potemplate
641 entry.pofile = pofile
642 # The uploaded file is only created in the librarian by a commit.
643 transaction.commit()
644 return entry
645
646 def test_translator_permissions(self):
647 # Sanity check that the translator has the right permissions but
648 # others don't.
649 pofile = self.factory.makePOFile(
650 self.language.code, potemplate=self.upstream_template)
651 self.assertFalse(
652 pofile.canEditTranslations(self.factory.makePerson()))
653 self.assertTrue(
654 pofile.canEditTranslations(self.translator.translator))
655
656 def test_makeImportEntry_templates_are_sharing(self):
657 # Sharing between upstream and Ubuntu was set up correctly.
658 entry = self._makeImportEntry(self.UBUNTU)
659 subset = getUtility(IPOTemplateSet).getSharingSubset(
660 distribution=entry.distroseries.distribution,
661 sourcepackagename=entry.sourcepackagename)
662 self.assertContentEqual(
663 [entry.potemplate, self.upstream_template],
664 list(subset.getSharingPOTemplates(entry.potemplate.name)))
665
666 def test_share_with_other_side_upstream(self):
667 # An upstream queue entry will be shared with ubuntu.
668 entry = self._makeImportEntry(self.UPSTREAM)
669 importer = POFileImporter(
670 entry, importers[TranslationFileFormat.PO], None)
671 self.assertTrue(
672 importer.share_with_other_side,
673 "Upstream import should share with Ubuntu.")
674
675 def test_share_with_other_side_ubuntu(self):
676 # An ubuntu queue entry will not be shared with upstream.
677 entry = self._makeImportEntry(self.UBUNTU)
678 importer = POFileImporter(
679 entry, importers[TranslationFileFormat.PO], None)
680 self.assertFalse(
681 importer.share_with_other_side,
682 "Ubuntu import should not share with upstream.")
683
684 def test_share_with_other_side_ubuntu_no_upstream(self):
685 # An ubuntu queue entry cannot share with a non-existent upstream.
686 entry = self._makeImportEntry(self.UBUNTU, no_upstream=True)
687 importer = POFileImporter(
688 entry, importers[TranslationFileFormat.PO], None)
689 self.assertFalse(
690 importer.share_with_other_side,
691 "Ubuntu import should not share with upstream.")
692
693 def test_share_with_other_side_ubuntu_from_package(self):
694 # An ubuntu queue entry that is imported from an upstream package
695 # will be shared with upstream.
696 entry = self._makeImportEntry(self.UBUNTU, from_upstream=True)
697 importer = POFileImporter(
698 entry, importers[TranslationFileFormat.PO], None)
699 self.assertTrue(
700 importer.share_with_other_side,
701 "Ubuntu import should share with upstream.")
702
703 def test_share_with_other_side_ubuntu_uploader_upstream_translator(self):
704 # If the uploader in ubuntu has rights on upstream as well, the
705 # translations are shared.
706 entry = self._makeImportEntry(
707 self.UBUNTU, uploader=self.translator.translator)
708 importer = POFileImporter(
709 entry, importers[TranslationFileFormat.PO], None)
710 self.assertTrue(
711 importer.share_with_other_side,
712 "Ubuntu import should share with upstream.")
565713
=== modified file 'lib/lp/translations/utilities/tests/test_sanitize.py'
--- lib/lp/translations/utilities/tests/test_sanitize.py 2010-08-23 08:41:03 +0000
+++ lib/lp/translations/utilities/tests/test_sanitize.py 2010-10-06 11:07:49 +0000
@@ -257,6 +257,20 @@
257 expected_sanitized,257 expected_sanitized,
258 sanitize_translations_from_webui(self.english, translations, 3))258 sanitize_translations_from_webui(self.english, translations, 3))
259259
260 def test_sanitize_translations_pluralforms_none(self):
261 # Some languages don't provide a plural form, so 2 is assumed.
262 translations = {
263 0: u'Plural form 0 ',
264 }
265 expected_sanitized = {
266 0: u'Plural form 0',
267 1: None,
268 }
269 self.assertEqual(
270 expected_sanitized,
271 sanitize_translations_from_webui(
272 self.english, translations, None))
273
260274
261def test_suite():275def test_suite():
262 return unittest.TestLoader().loadTestsFromName(__name__)276 return unittest.TestLoader().loadTestsFromName(__name__)
263277
=== modified file 'lib/lp/translations/utilities/translation_import.py'
--- lib/lp/translations/utilities/translation_import.py 2010-09-06 10:40:54 +0000
+++ lib/lp/translations/utilities/translation_import.py 2010-10-06 11:07:49 +0000
@@ -31,6 +31,11 @@
31 PersonCreationRationale,31 PersonCreationRationale,
32 )32 )
33from lp.services.propertycache import cachedproperty33from lp.services.propertycache import cachedproperty
34from lp.translations.interfaces.potemplate import IPOTemplateSet
35from lp.translations.interfaces.side import (
36 ITranslationSideTraitsSet,
37 TranslationSide,
38 )
34from lp.translations.interfaces.translationexporter import (39from lp.translations.interfaces.translationexporter import (
35 ITranslationExporter,40 ITranslationExporter,
36 )41 )
@@ -45,15 +50,25 @@
45from lp.translations.interfaces.translationimportqueue import (50from lp.translations.interfaces.translationimportqueue import (
46 RosettaImportStatus,51 RosettaImportStatus,
47 )52 )
48from lp.translations.interfaces.translationmessage import TranslationConflict53from lp.translations.interfaces.translationmessage import (
54 RosettaTranslationOrigin,
55 TranslationConflict,
56 TranslationValidationStatus,
57 )
49from lp.translations.interfaces.translations import TranslationConstants58from lp.translations.interfaces.translations import TranslationConstants
50from lp.translations.utilities.gettext_po_importer import GettextPOImporter59from lp.translations.utilities.gettext_po_importer import GettextPOImporter
51from lp.translations.utilities.kde_po_importer import KdePOImporter60from lp.translations.utilities.kde_po_importer import KdePOImporter
52from lp.translations.utilities.mozilla_xpi_importer import MozillaXpiImporter61from lp.translations.utilities.mozilla_xpi_importer import MozillaXpiImporter
62from lp.translations.utilities.sanitize import (
63 sanitize_translations_from_import,
64 )
53from lp.translations.utilities.translation_common_format import (65from lp.translations.utilities.translation_common_format import (
54 TranslationMessageData,66 TranslationMessageData,
55 )67 )
56from lp.translations.utilities.validate import GettextValidationError68from lp.translations.utilities.validate import (
69 GettextValidationError,
70 validate_translation,
71 )
5772
5873
59importers = {74importers = {
@@ -445,7 +460,89 @@
445 context=message.context))460 context=message.context))
446 return potmsgset461 return potmsgset
447462
448 def storeTranslationsInDatabase(self, message, potmsgset):463 @cachedproperty
464 def share_with_other_side(self):
465 """Returns True if translations should be shared with the other side.
466 """
467 traits = getUtility(
468 ITranslationSideTraitsSet).getForTemplate(self.potemplate)
469 if traits.side == TranslationSide.UPSTREAM:
470 return True
471 # Check from_upstream.
472 if self.translation_import_queue_entry.from_upstream:
473 return True
474 # Find the sharing POFile and check permissions.
475 productseries = self.potemplate.distroseries.getSourcePackage(
476 self.potemplate.sourcepackagename).productseries
477 if productseries is None:
478 return False
479 upstream_template = getUtility(IPOTemplateSet).getSubset(
480 productseries=productseries).getPOTemplateByName(
481 self.potemplate.name)
482 upstream_pofile = upstream_template.getPOFileByLang(
483 self.pofile.language.code)
484 if upstream_pofile is not None:
485 uploader_person = self.translation_import_queue_entry.importer
486 if upstream_pofile.canEditTranslations(uploader_person):
487 return True
488 # Deny the rest.
489 return False
490
491 @cachedproperty
492 def translations_are_msgids(self):
493 """Are these English strings instead of translations?
494
495 If this template uses symbolic message ids, the English POFile
496 will contain the English original texts that correspond to the
497 symbols."""
498 return (
499 self.importer.uses_source_string_msgids and
500 self.pofile.language.code == 'en')
501
502 def _storeCredits(self, potmsgset, credits):
503 """Store credits but only from upstream."""
504 if not self.translation_import_queue_entry.from_upstream:
505 return None
506 return potmsgset.setCurrentTranslation(
507 self.pofile, self.last_translator, credits,
508 RosettaTranslationOrigin.SCM, self.share_with_other_side)
509
510 def _validateMessage(self, potmsgset, message,
511 translations, message_data):
512 """Validate the message and report success or failure."""
513 try:
514 validate_translation(
515 potmsgset.singular_text, potmsgset.plural_text,
516 translations, potmsgset.flags)
517 except GettextValidationError, e:
518 self._addUpdateError(message_data, potmsgset, unicode(e))
519 message.validation_status = (
520 TranslationValidationStatus.UNKNOWNERROR)
521 return False
522
523 message.validation_status = TranslationValidationStatus.OK
524 return True
525
526 def _approveMessage(self, potmsgset, message, message_data):
527 """Try to approve the message, return None on TranslationConflict."""
528 try:
529 message.approve(
530 self.pofile, self.last_translator,
531 self.share_with_other_side, self.lock_timestamp)
532 except TranslationConflict:
533 self._addConflictError(message_data, potmsgset)
534 if self.logger is not None:
535 self.logger.info(
536 "Conflicting updates on message %d." % potmsgset.id)
537 # The message remains a suggestion.
538 return None
539
540 if self.translations_are_msgids:
541 potmsgset.clearCachedSingularText()
542
543 return message
544
545 def storeTranslationsInDatabase(self, message_data, potmsgset):
449 """Try to store translations in the database.546 """Try to store translations in the database.
450547
451 Perform check if a PO file is available and if the message has any548 Perform check if a PO file is available and if the message has any
@@ -453,7 +550,8 @@
453 is added to the list in self.errors but the translations are stored550 is added to the list in self.errors but the translations are stored
454 anyway, marked as having an error.551 anyway, marked as having an error.
455552
456 :param message: The message for which translations will be stored.553 :param message_data: The message data for which translations will be
554 stored.
457 :param potmsgset: The POTMsgSet that this message belongs to.555 :param potmsgset: The POTMsgSet that this message belongs to.
458556
459 :return: The updated translation_message entry or None, if no storing557 :return: The updated translation_message entry or None, if no storing
@@ -464,54 +562,39 @@
464 # store English strings in an IPOFile.562 # store English strings in an IPOFile.
465 return None563 return None
466564
467 if (not message.translations or565 no_translations = (
468 set(message.translations) == set([u'']) or566 message_data.translations is None or
469 set(message.translations) == set([None])):567 not any(message_data.translations))
568 if no_translations:
470 # We don't have anything to import.569 # We don't have anything to import.
471 return None570 return None
472571
473 try:572 sanitized_translations = sanitize_translations_from_import(
474 # Do the actual import.573 potmsgset.singular_text, message_data.translations,
475 translation_message = potmsgset.updateTranslation(574 self.pofile.language.pluralforms)
476 self.pofile, self.last_translator, message.translations,575
477 self.translation_import_queue_entry.from_upstream,576 if potmsgset.is_translation_credit:
478 self.lock_timestamp, force_edition_rights=self.is_editor)577 # Translation credits cannot be added as suggestions.
479 except TranslationConflict:578 return self._storeCredits(potmsgset, sanitized_translations)
480 self._addConflictError(message, potmsgset)579
481 if self.logger is not None:580 # The message is first stored as a suggestion and only made
482 self.logger.info(581 # current if it validates.
483 "Conflicting updates on message %d." % potmsgset.id)582 new_message = potmsgset.submitSuggestion(
484 return None583 self.pofile, self.last_translator, sanitized_translations)
485 except GettextValidationError, e:584
486 # We got an error, so we submit the translation again but585 validation_ok = self._validateMessage(
487 # this time asking to store it as a translation with586 potmsgset, new_message, sanitized_translations, message_data)
488 # errors.587 if validation_ok and self.is_editor:
489588 return self._approveMessage(potmsgset, new_message, message_data)
490 # Add the pomsgset to the list of pomsgsets with errors.589
491 self._addUpdateError(message, potmsgset, unicode(e))590 if self.translation_import_queue_entry.from_upstream:
492591 # XXX: henninge 2010-09-21: Mixed models!
493 try:592 # This is mimicking the old behavior to still mark these messages
494 translation_message = potmsgset.updateTranslation(593 # as "imported". Will have to be removed when
495 self.pofile, self.last_translator, message.translations,594 # getPOTMsgSetsWithErrors is updated to the new model.
496 self.translation_import_queue_entry.from_upstream,595 new_message.makeCurrentUpstream(True)
497 self.lock_timestamp, ignore_errors=True,596
498 force_edition_rights=self.is_editor)597 return new_message
499 except TranslationConflict:
500 # A conflict on top of a validation error? Give up.
501 # This message is cursed.
502 if self.logger is not None:
503 self.logger.info(
504 "Conflicting updates; ignoring invalid message %d." %
505 potmsgset.id)
506 return None
507
508 just_replaced_msgid = (
509 self.importer.uses_source_string_msgids and
510 self.pofile.language.code == 'en')
511 if just_replaced_msgid:
512 potmsgset.clearCachedSingularText()
513
514 return translation_message
515598
516 def importMessage(self, message):599 def importMessage(self, message):
517 """Import a single message.600 """Import a single message.

Subscribers

People subscribed via source and target branches