Merge lp:~adiroiban/launchpad/bug-427319 into lp:launchpad

Proposed by Adi Roiban
Status: Merged
Approved by: Guilherme Salgado
Approved revision: no longer in the source branch.
Merged at revision: not available
Proposed branch: lp:~adiroiban/launchpad/bug-427319
Merge into: lp:launchpad
Diff against target: 619 lines (+365/-103)
7 files modified
lib/lp/translations/browser/configure.zcml (+5/-6)
lib/lp/translations/browser/productserieslanguage.py (+0/-57)
lib/lp/translations/browser/serieslanguage.py (+124/-10)
lib/lp/translations/browser/tests/test_distroserieslanguage_views.py (+5/-1)
lib/lp/translations/browser/tests/test_productserieslanguage_views.py (+3/-1)
lib/lp/translations/stories/standalone/xx-serieslanguage-index.txt (+189/-1)
lib/lp/translations/templates/serieslanguage-index.pt (+39/-27)
To merge this branch: bzr merge lp:~adiroiban/launchpad/bug-427319
Reviewer Review Type Date Requested Status
Guilherme Salgado (community) code Approve
Martin Albisetti (community) ui Approve
Review via email: mp+15994@code.launchpad.net

Commit message

Combine the ProductSeriesLanguage and DistroSeriesLanguage in a common view. Inform translators about what they can do for that series translations.

To post a comment you must log in.
Revision history for this message
Adi Roiban (adiroiban) wrote :

= Bug 427319 =

The current note are a bit generic and also they refer to group coordinator, while linking to the group page (note to the group coordinator)

== Proposed fix ==

As a start add a direct link to team contact.

If there is no team to manage the translations for a language, inform translators and refer the person that can be contacted for setting up a new group.

Inform translators that they need to log in in order to make translations.

Inform translators that their suggestion needs to be approved by the managing team and link to the team page.

== Implementation details ==

Since productseries-langauge and distroseries-language views were very similar, I created a SeriesLangauge View inherited by both ProductSeries and DistroSeries.

== Tests ==
New tests were added to cover the added functionality.

./bin/test -vvc -m translations --layer PageTestLayer

== Demo and Q/A ==
Log in as an Admin and change Ubuntu settings so that there is no translation group for Ubuntu
https://translations.launchpad.dev/ubuntu/+settings

Then go to these pages with an anonymous, an authententicated and admin users.
Observe how the note on top of the templates table changes to reflect you current access to the translations.

http://translations.launchpad.dev/ubuntu/hoary/+lang/es
http://translations.launchpad.dev/ubuntu/hoary/+lang/pt_BR

There is a detailed testplan in standalone/xx-serieslanguage-index.pt

= Launchpad lint =

Checking for conflicts. and issues in doctests and templates.
Running jslint, xmllint, pyflakes, and pylint.
Using normal rules.

Linting changed files:
  lib/lp/translations/browser/serieslanguage.py
  lib/lp/translations/templates/serieslanguage-index.pt

Revision history for this message
Guilherme Salgado (salgado) wrote :
Download full text (19.2 KiB)

Hi Adi,

This is a nice improvement to Launchpad's UI, but is also a very nice
refactoring -- I love when I see duplicated code being removed.

I think there's some room for us to improve the way the access-level
messages are generated, and I also have a few stylistic recommendations
below. Please let me know what you think.

 review needsfixing

On Fri, 2009-12-11 at 01:30 +0000, Adi Roiban wrote:
> = Bug 427319 =
>
> The current note are a bit generic and also they refer to group
> coordinator, while linking to the group page (note to the group
> coordinator)
>
> == Proposed fix ==
>
> As a start add a direct link to team contact.
>
> If there is no team to manage the translations for a language, inform
> translators and refer the person that can be contacted for setting up
> a new group.
>
> Inform translators that they need to log in in order to make
> translations.
>
> Inform translators that their suggestion needs to be approved by the
> managing team and link to the team page.
>
> == Implementation details ==
>
> Since productseries-langauge and distroseries-language views were very
> similar, I created a SeriesLangauge View inherited by both
> ProductSeries and DistroSeries.
>

> === removed file 'lib/lp/translations/browser/productserieslanguage.py'
> --- lib/lp/translations/browser/productserieslanguage.py 2009-09-17 12:45:52 +0000
> +++ lib/lp/translations/browser/productserieslanguage.py 1970-01-01 00:00:00 +0000
> @@ -1,57 +0,0 @@
> -# Copyright 2009 Canonical Ltd. This software is licensed under the
> -# GNU Affero General Public License version 3 (see the file LICENSE).
> -
> -"""Browser code for Product Series Languages."""
> -
> -__metaclass__ = type
> -
> -__all__ = [
> - 'ProductSeriesLanguageNavigation',
> - 'ProductSeriesLanguageView',
> - ]
> -
> -from canonical.cachedproperty import cachedproperty
> -from canonical.launchpad.webapp import LaunchpadView
> -from canonical.launchpad.webapp.batching import BatchNavigator
> -from canonical.launchpad.webapp.publisher import Navigation
> -from lp.translations.interfaces.productserieslanguage import (
> - IProductSeriesLanguage)
> -
> -
> -class ProductSeriesLanguageView(LaunchpadView):
> - """View class to render translation status for an `IProductSeries`."""
> -
> - pofiles = None
> - label = "Translatable templates"
> -
> - def initialize(self):
> - self.form = self.request.form
> -
> - self.batchnav = BatchNavigator(
> - self.context.productseries.getCurrentTranslationTemplates(),
> - self.request)
> -
> - self.context.recalculateCounts()
> -
> - self.pofiles = self.context.getPOFilesFor(
> - self.batchnav.currentBatch())
> - self.parent = self.context.productseries.product
> -
> - @cachedproperty
> - def translation_group(self):
> - return self.context.productseries.product.translationgroup
> -
> - @cachedproperty
> - def translation_team(self):
> - """Is there a translation team for this translation."""
> - if self.translation_group is not None:
> - team = self.translation_group.query_translator(
> - ...

review: Needs Fixing
Revision history for this message
Adi Roiban (adiroiban) wrote :
Download full text (19.0 KiB)

Many thanks for the review.
Here is the diff.

=== modified file 'lib/lp/translations/browser/serieslanguage.py'
--- lib/lp/translations/browser/serieslanguage.py 2009-12-11 01:12:22 +0000
+++ lib/lp/translations/browser/serieslanguage.py 2009-12-11 16:09:04 +0000
@@ -14,6 +14,7 @@

 from canonical.cachedproperty import cachedproperty
 from canonical.launchpad.webapp import LaunchpadView
+from canonical.launchpad.webapp.tales import PersonFormatterAPI
 from canonical.launchpad.webapp.batching import BatchNavigator
 from canonical.launchpad.webapp.publisher import Navigation
 from lp.translations.interfaces.distroserieslanguage import (
@@ -26,9 +27,12 @@
     IProductSeriesLanguage)

-class SeriesLanguageView(LaunchpadView):
- """View class to render translation status for an `IDistroSeries`
- and `IProductSeries`"""
+class BaseSeriesLanguageView(LaunchpadView):
+ """View base class to render translation status for an
+ `IDistroSeries` and `IProductSeries`
+
+ This class should not be directly instantiated.
+ """

     pofiles = None
     label = "Translatable templates"
@@ -36,7 +40,9 @@
     parent = None
     translationgroup = None

- def initialize(self):
+ def initialize(self, series, translationgroup):
+ self.series = series
+ self.translationgroup = translationgroup
         self.form = self.request.form

         self.batchnav = BatchNavigator(
@@ -46,14 +52,20 @@
         self.pofiles = self.context.getPOFilesFor(
             self.batchnav.currentBatch())

- @cachedproperty
+ @property
     def translation_group(self):
- """Is there a translation group for these translations."""
+ """Return the translation group for these translations.
+
+ Return None if there's no translation group for them.
+ """
         return self.translationgroup

     @cachedproperty
     def translation_team(self):
- """Is there a translation team for these translations."""
+ """Return the translation team for these translations.
+
+ Return None if there's no translation team for them.
+ """
         if self.translation_group is not None:
             team = self.translation_group.query_translator(
                 self.context.language)
@@ -62,98 +74,80 @@
         return team

     @property
- def show_not_logged_in(self):
- """Should we display a notice that user is not logged in?"""
- return self.user is None
+ def access_level_description(self):
+ if self.user is None:
+ return ("You are not logged in. Please log in to work " +
+ "on translations.")

- @property
- def show_no_license(self):
- """Should we display a notice that licence was not accepted?"""
- if self.show_not_logged_in:
- return False
         translations_person = ITranslationsPerson(self.user)
- return not translations_person.translations_relicensing_agreement
-
- @property
- def show_full_edit(self):
- """Should we display a notice that user is not logged in?"""
- if (self.show_not_logged_in or
- self.show_no_license):
- return False
-
- ...

Revision history for this message
Guilherme Salgado (salgado) wrote :
Download full text (15.1 KiB)

Hi Adi,

I have just a few minor suggestions but this will probably be the last
round; this branch should be ready to land once they're addressed.

> === renamed file 'lib/lp/translations/browser/distroserieslanguage.py' => 'lib/lp/translations/browser/serieslanguage.py'
> --- lib/lp/translations/browser/distroserieslanguage.py 2009-09-17 11:10:49 +0000
> +++ lib/lp/translations/browser/serieslanguage.py 2009-12-11 16:18:29 +0000
>
> +
> +class BaseSeriesLanguageView(LaunchpadView):
[...]
> @@ -47,7 +73,88 @@
> team = None
> return team
>
> + @property
> + def access_level_description(self):
> + if self.user is None:
> + return ("You are not logged in. Please log in to work " +
> + "on translations.")

When you use parenthesis for multi-line strings in python, they're
concatenated automatically, so you don't need the '+' here.

> +
> + translations_person = ITranslationsPerson(self.user)
> + translations_contact_link = None
> +
> + if self.translation_team:
> + translations_contact_link = PersonFormatterAPI(
> + self.translation_team.translator).link(None)
> + elif self.translation_group:
> + translations_contact_link = PersonFormatterAPI(
> + self.translation_group.owner).link(None)

Is it possible that self.translation_team and self.translation_group are
both None, thus leaving translations_contact_link set to None as well?
I hope that's not possible as it'd cause the code below to crash, and in
that case we can make this assumption (that we expect at least one of
them to be non-None) clear in the code by adding an else block with an
AssertionError.

    else:
        raise AssertionError("No translation team/group found.")

And then you can also remove the "translations_contact_link = None"
line.

> +
> + if not translations_person.translations_relicensing_agreement:
> + translation_license_url = PersonFormatterAPI(
> + translations_person).url() + '/translations/+licensing'

Have you tried calling .url(view_name='+licensing') here? In fact, I
think you need that because the URL generated by the code above is wrong
(e.g. ~user/translations/+licensing, which is a 404). If you use the
view_name argument it will make sure the generated URL is not a 404.

> + return ("To make translations in Launchpad you need to " +
> + "agree with the " +

No need to add the strings here either. :)

> + "<a href='%s'>Translations licensing</a>.") % (
> + translation_license_url)
> +
> + sample_pofile = self.pofiles[0]
> + if sample_pofile is not None:
> + if sample_pofile.canEditTranslations(self.user):
> + return "You can add and review translations."
> +
> + if sample_pofile.canAddSuggestions(self.user):
> + return ("Your suggestions will be held for review by " +
> + "the managers of these translations. If you " +
> + "need help, or your translations are not being " +
> + "...

Revision history for this message
Martin Albisetti (beuno) wrote :

11:50 < beuno> adiroiban, nice improvement
11:51 < beuno> a few questions
11:51 < beuno> why not "Your suggestions will be held for review by $TEAM"?
11:53 < beuno> there's also a missing full stop
11:53 < beuno> after the team's name
11:53 < beuno> before "Templates which are..."
11:53 < beuno> other than that, ui=me
11:54 < adiroiban> beuno: ok. I will change that.

review: Approve (ui)
Revision history for this message
Данило Шеган (danilo) wrote :

Btw, nice refactoring Adi. Thanks for doing it! It'd be nice to move it even farther where we register only a single view (SeriesLanguageView) for all ISeriesLanguage-implementing objects (note, this interface doesn't exist yet :), but let's leave that for some other time :)

Also, now that these views are unified, it makes sense to also unify the view tests: i.e. I believe mostly setUp method would have to be changed to accommodate different ways to construct distroserieslanguages and productserieslanguages, but that would make it easy to extend these views to sourcepackagelanguage and projectlanguage views in the future.

Revision history for this message
Adi Roiban (adiroiban) wrote :
Download full text (29.2 KiB)

În data de Vi, 11-12-2009 la 17:48 +0000, Guilherme Salgado a scris:
> Hi Adi,
>
> I have just a few minor suggestions but this will probably be the last
> round; this branch should be ready to land once they're addressed.
>
> > === renamed file 'lib/lp/translations/browser/distroserieslanguage.py' => 'lib/lp/translations/browser/serieslanguage.py'
> > --- lib/lp/translations/browser/distroserieslanguage.py 2009-09-17 11:10:49 +0000
> > +++ lib/lp/translations/browser/serieslanguage.py 2009-12-11 16:18:29 +0000
> >
> > +
> > +class BaseSeriesLanguageView(LaunchpadView):
> [...]
> > @@ -47,7 +73,88 @@
> > team = None
> > return team
> >
> > + @property
> > + def access_level_description(self):
> > + if self.user is None:
> > + return ("You are not logged in. Please log in to work " +
> > + "on translations.")
>
> When you use parenthesis for multi-line strings in python, they're
> concatenated automatically, so you don't need the '+' here.
Done.

> > +
> > + translations_person = ITranslationsPerson(self.user)
> > + translations_contact_link = None
> > +
> > + if self.translation_team:
> > + translations_contact_link = PersonFormatterAPI(
> > + self.translation_team.translator).link(None)
> > + elif self.translation_group:
> > + translations_contact_link = PersonFormatterAPI(
> > + self.translation_group.owner).link(None)
>
> Is it possible that self.translation_team and self.translation_group are
> both None, thus leaving translations_contact_link set to None as well?
> I hope that's not possible as it'd cause the code below to crash, and in
> that case we can make this assumption (that we expect at least one of
> them to be non-None) clear in the code by adding an else block with an
> AssertionError.
>
> else:
> raise AssertionError("No translation team/group found.")
>
> And then you can also remove the "translations_contact_link = None"
> line.

It is possible and it should not be an error.
In such case no information should be display.

In a normal use case we should not reach this line, as that check is
already done in the template.

I added a check and returned "".

> > +
> > + if not translations_person.translations_relicensing_agreement:
> > + translation_license_url = PersonFormatterAPI(
> > + translations_person).url() + '/translations/+licensing'
>
> Have you tried calling .url(view_name='+licensing') here? In fact, I
> think you need that because the URL generated by the code above is wrong
> (e.g. ~user/translations/+licensing, which is a 404). If you use the
> view_name argument it will make sure the generated URL is not a 404.
>
> > + return ("To make translations in Launchpad you need to " +
> > + "agree with the " +
>
> No need to add the strings here either. :)
Done
> > + "<a href='%s'>Translations licensing</a>.") % (
> > + translation_license_url)
> > +
> > + sample_pofile = self.pofiles[0]
> > + if sample_pofile is not None:
> > + ...

Revision history for this message
Guilherme Salgado (salgado) wrote :
Download full text (9.6 KiB)

Hi Adi,

Just a couple more suggestions, but these are really trivial. Once you
push the changes I'll submit your branch to ec2 to run the full test
suite.

Once again, thanks a lot for this improvement and the very nice cleanup!

 review approve
 status approved

On Fri, 2009-12-11 at 19:54 +0000, Adi Roiban wrote:
> În data de Vi, 11-12-2009 la 17:48 +0000, Guilherme Salgado a scris:

> > > +
> > > + translations_person = ITranslationsPerson(self.user)
> > > + translations_contact_link = None
> > > +
> > > + if self.translation_team:
> > > + translations_contact_link = PersonFormatterAPI(
> > > + self.translation_team.translator).link(None)
> > > + elif self.translation_group:
> > > + translations_contact_link = PersonFormatterAPI(
> > > + self.translation_group.owner).link(None)
> >
> > Is it possible that self.translation_team and self.translation_group are
> > both None, thus leaving translations_contact_link set to None as well?
> > I hope that's not possible as it'd cause the code below to crash, and in
> > that case we can make this assumption (that we expect at least one of
> > them to be non-None) clear in the code by adding an else block with an
> > AssertionError.
> >
> > else:
> > raise AssertionError("No translation team/group found.")
> >
> > And then you can also remove the "translations_contact_link = None"
> > line.
>
> It is possible and it should not be an error.
> In such case no information should be display.
>
> In a normal use case we should not reach this line, as that check is
> already done in the template.
>
> I added a check and returned "".

In this case I think the best is to document that in the method's
docstring (e.g. "This method must not be called if
self.translation_group is None.") and turn the if/elif into an if/else
with an assert, like this:

    if self.translation_team:
        ...
    else:
        assert self.translation_group is not None, (
            "Must not be called when there's no translation group.")
        ...

> > > +class DistroSeriesLanguageView(BaseSeriesLanguageView, LaunchpadView):
> > > + """View class to render translation status for an `IDistroSeries`."""
> > > +
> > > + def initialize(self):
> > > + series = self.context.distroseries
> > > + super(DistroSeriesLanguageView, self).initialize(
> > > + series=series,
> > > + translationgroup=series.distribution.translationgroup)
> > > + self.parent = self.series.distribution
> > > +
> > > +
> > > +class ProductSeriesLanguageView(BaseSeriesLanguageView, LaunchpadView):
> > > + """View class to render translation status for an `IProductSeries`."""
> > > +
> > > + def initialize(self):
> > > + series = self.context.productseries
> > > + super(ProductSeriesLanguageView, self).initialize(
> > > + series=series,
> >
> > I'd drop the 'series = self.context....' line in both classes above and
> > just pass series=self.context... to super's initialize().
>
> I have leave that attribution to avoid a long line that will follow.
>
> translationgroup=self.context.pro...

Read more...

review: Approve
Revision history for this message
Adi Roiban (adiroiban) wrote :
Download full text (9.1 KiB)

În data de Vi, 11-12-2009 la 20:18 +0000, Guilherme Salgado a scris:
> Review: Approve
> Hi Adi,
>
> Just a couple more suggestions, but these are really trivial. Once you
> push the changes I'll submit your branch to ec2 to run the full test
> suite.
>
> Once again, thanks a lot for this improvement and the very nice cleanup!
>
> review approve
> status approved
[snip]
> In this case I think the best is to document that in the method's
> docstring (e.g. "This method must not be called if
> self.translation_group is None.") and turn the if/elif into an if/else
> with an assert, like this:
>
> if self.translation_team:
> ...
> else:
> assert self.translation_group is not None, (
> "Must not be called when there's no translation group.")
> ...
OK. Done.

> This check can go away once the if/elif is converted into an if/else
> with an assertion. The latter is preferred because it will fail
> horribly (instead of silently, like it currently does) when the property
> is used incorrectly (i.e. when translation_group is None).
Yep.

> > +
> > if not translations_person.translations_relicensing_agreement:
> > translation_license_url = PersonFormatterAPI(
> > - translations_person).url() + '/translations/+licensing'
> > - return ("To make translations in Launchpad you need to " +
> > - "agree with the " +
> > + translations_person).url(
> > + view_name='+licensing',
> > + rootsite='translations')
>
> You might not need rootsite='translations' (as this page is already on
> the translations rootsite), but I might be wrong.
I've checked the implementation for PersonFormatterAPI.url, and as far
I can understand that code, the rootsite is not automatically detected.

rootsite is required... as +licensing is not valid for main.

Also tested without rootsite and it was not created for translations.

> > +Users will see three references to the team managing these translations.
> > +
> > + >>> print user_browser.getLink(
> > + ... 'Ubuntu Spanish Translator',index=0).url
> > + http://launchpad.dev/~ubuntu-l10n-es
> > +
> > + >>> print user_browser.getLink(
> > + ... 'Ubuntu Spanish Translator',index=1).url
> > + http://launchpad.dev/~ubuntu-l10n-es
> > +
> > + >>> print user_browser.getLink(
> > + ... 'Ubuntu Spanish Translator',index=2).url
> > + http://launchpad.dev/~ubuntu-l10n-es
>
> Is it really important to show that we have 3 links to the team managing
> the translations?
Fixed.

I added new tests for checking when a translator has not agreed the
translation license and when translations are CLOSED.

Merged with devel since I modified the Factory in a branch that was just
commited and those changes also affects these tests.

Cheers.

Here is the diff.

=== modified file 'lib/lp/translations/browser/serieslanguage.py'
--- lib/lp/translations/browser/serieslanguage.py 2009-12-11 20:11:06
+0000
+++ lib/lp/translations/browser/serieslanguage.py 2009-12-12 06:19:24
+0000
@@ -75,6 +75,8 @@

     @property
     def access_level_description(self):
+...

Read more...

Revision history for this message
Guilherme Salgado (salgado) :
review: Approve (code)
Revision history for this message
Adi Roiban (adiroiban) wrote :

Hi Salgado,

The failing test is /home/adi/launchpad/lp-branches/bug-427319/lib/lp/translations/browser/tests/test_distroserieslanguage_views.py, line 51, in test_translation_group

    def test_translation_group(self):
        group = self.factory.makeTranslationGroup(
            self.distroseries.distribution.owner, url=None)
        self.distroseries.distribution.translationgroup = group
        self.assertEquals(self.view.translation_group, group)

I tried to change it to the following code... but with no luck

    def test_translation_group(self):
        group = self.factory.makeTranslationGroup(
            self.distroseries.distribution.owner, url=None)
        self.distroseries.distribution.translationgroup = group
        self.view = DistroSeriesLanguageView(
            self.dsl, LaunchpadTestRequest())
        self.assertEquals(self.view.translation_group, group)

--------------------
This is a fix for the view, but maybe I have to fix the test.

=== modified file 'lib/lp/translations/browser/serieslanguage.py'
--- lib/lp/translations/browser/serieslanguage.py 2009-12-12 07:16:07 +0000
+++ lib/lp/translations/browser/serieslanguage.py 2009-12-15 02:25:54 +0000
@@ -38,11 +38,9 @@
     label = "Translatable templates"
     series = None
     parent = None
- translationgroup = None

     def initialize(self, series, translationgroup):
         self.series = series
- self.translationgroup = translationgroup
         self.form = self.request.form

         self.batchnav = BatchNavigator(
@@ -58,7 +56,10 @@

         Return None if there's no translation group for them.
         """
- return self.translationgroup
+ if (self.context.distroseries):
+ return self.context.distroseries.distribution.translationgroup
+ else:
+ return self.context.productseries.product.translationgroup

     @cachedproperty
     def translation_team(self):
@@ -134,25 +135,21 @@
             "translations.")

-class DistroSeriesLanguageView(BaseSeriesLanguageView, LaunchpadView):
+class DistroSeriesLanguageView(BaseSeriesLanguageView):
     """View class to render translation status for an `IDistroSeries`."""

     def initialize(self):
- series = self.context.distroseries
         super(DistroSeriesLanguageView, self).initialize(
- series=series,
- translationgroup=series.distribution.translationgroup)
+ series=self.context.distroseries)
         self.parent = self.series.distribution

-class ProductSeriesLanguageView(BaseSeriesLanguageView, LaunchpadView):
+class ProductSeriesLanguageView(BaseSeriesLanguageView):
     """View class to render translation status for an `IProductSeries`."""

     def initialize(self):
- series = self.context.productseries
         super(ProductSeriesLanguageView, self).initialize(
- series=series,
- translationgroup=series.product.translationgroup)
+ series=self.context.productseries)
         self.context.recalculateCounts()
         self.parent = self.series.product

Revision history for this message
Adi Roiban (adiroiban) wrote :

I start a full test.

Here is the diff for the tests.

=== modified file 'lib/lp/translations/browser/tests/test_distroserieslanguage_views.py'
--- lib/lp/translations/browser/tests/test_distroserieslanguage_views.py 2009-12-11 01:03:25 +0000
+++ lib/lp/translations/browser/tests/test_distroserieslanguage_views.py 2009-12-17 15:05:44 +0000
@@ -48,6 +48,8 @@
         group = self.factory.makeTranslationGroup(
             self.distroseries.distribution.owner, url=None)
         self.distroseries.distribution.translationgroup = group
+ self.view = DistroSeriesLanguageView(
+ self.dsl, LaunchpadTestRequest()).initialize()
         self.assertEquals(self.view.translation_group, group)

     def test_translation_team(self):
@@ -65,7 +67,7 @@
             group, self.language, team)
         # Recreate the view because we are using a cached property.
         self.view = DistroSeriesLanguageView(
- self.dsl, LaunchpadTestRequest())
+ self.dsl, LaunchpadTestRequest()).initialize()
         self.assertEquals(self.view.translation_team, translator)

 def test_suite():

=== modified file 'lib/lp/translations/browser/tests/test_productserieslanguage_views.py'
--- lib/lp/translations/browser/tests/test_productserieslanguage_views.py 2009-12-11 16:09:04 +0000
+++ lib/lp/translations/browser/tests/test_productserieslanguage_views.py 2009-12-17 15:07:42 +0000
@@ -137,6 +137,8 @@
         group = self.factory.makeTranslationGroup(
             self.productseries.product.owner, url=None)
         self.productseries.product.translationgroup = group
+ self.view = ProductSeriesLanguageView(
+ self.psl, LaunchpadTestRequest()).initialize()
         self.assertEquals(self.view.translation_group, group)

     def test_translation_team(self):
@@ -154,7 +156,7 @@
             group, self.language, team)
         # Recreate the view because we are using a cached property.
         self.view = ProductSeriesLanguageView(
- self.psl, LaunchpadTestRequest())
+ self.psl, LaunchpadTestRequest()).initialize()
         self.assertEquals(self.view.translation_team, translator)

 def test_suite():

Revision history for this message
Adi Roiban (adiroiban) wrote :

I did a full test using the latest branch and everything is OK now. We will see the truth on Monday :)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/translations/browser/configure.zcml'
2--- lib/lp/translations/browser/configure.zcml 2009-12-12 02:14:16 +0000
3+++ lib/lp/translations/browser/configure.zcml 2009-12-17 16:29:14 +0000
4@@ -161,9 +161,8 @@
5 attribute_to_parent="distroseries"
6 rootsite="translations"/>
7 <browser:navigation
8- module="lp.translations.browser.distroserieslanguage"
9- classes="
10- DistroSeriesLanguageNavigation"/>
11+ module="lp.translations.browser.serieslanguage"
12+ classes="DistroSeriesLanguageNavigation"/>
13 <browser:defaultView
14 for="lp.translations.interfaces.distroserieslanguage.IDistroSeriesLanguage"
15 name="+index"
16@@ -182,7 +181,7 @@
17 permission="zope.Public"
18 for="lp.translations.interfaces.distroserieslanguage.IDistroSeriesLanguage"
19 template="../templates/serieslanguage-index.pt"
20- class="lp.translations.browser.distroserieslanguage.DistroSeriesLanguageView"
21+ class="lp.translations.browser.serieslanguage.DistroSeriesLanguageView"
22 facet="translations"
23 layer="canonical.launchpad.layers.TranslationsLayer"/>
24 <facet
25@@ -272,7 +271,7 @@
26 attribute_to_parent="productseries"
27 rootsite="translations"/>
28 <browser:navigation
29- module="lp.translations.browser.productserieslanguage"
30+ module="lp.translations.browser.serieslanguage"
31 classes="
32 ProductSeriesLanguageNavigation"/>
33 <browser:defaultView
34@@ -292,7 +291,7 @@
35 permission="zope.Public"
36 for="lp.translations.interfaces.productserieslanguage.IProductSeriesLanguage"
37 template="../templates/serieslanguage-index.pt"
38- class="lp.translations.browser.productserieslanguage.ProductSeriesLanguageView"
39+ class="lp.translations.browser.serieslanguage.ProductSeriesLanguageView"
40 facet="translations"
41 layer="canonical.launchpad.layers.TranslationsLayer"/>
42 <facet
43
44=== removed file 'lib/lp/translations/browser/productserieslanguage.py'
45--- lib/lp/translations/browser/productserieslanguage.py 2009-09-17 12:45:52 +0000
46+++ lib/lp/translations/browser/productserieslanguage.py 1970-01-01 00:00:00 +0000
47@@ -1,57 +0,0 @@
48-# Copyright 2009 Canonical Ltd. This software is licensed under the
49-# GNU Affero General Public License version 3 (see the file LICENSE).
50-
51-"""Browser code for Product Series Languages."""
52-
53-__metaclass__ = type
54-
55-__all__ = [
56- 'ProductSeriesLanguageNavigation',
57- 'ProductSeriesLanguageView',
58- ]
59-
60-from canonical.cachedproperty import cachedproperty
61-from canonical.launchpad.webapp import LaunchpadView
62-from canonical.launchpad.webapp.batching import BatchNavigator
63-from canonical.launchpad.webapp.publisher import Navigation
64-from lp.translations.interfaces.productserieslanguage import (
65- IProductSeriesLanguage)
66-
67-
68-class ProductSeriesLanguageView(LaunchpadView):
69- """View class to render translation status for an `IProductSeries`."""
70-
71- pofiles = None
72- label = "Translatable templates"
73-
74- def initialize(self):
75- self.form = self.request.form
76-
77- self.batchnav = BatchNavigator(
78- self.context.productseries.getCurrentTranslationTemplates(),
79- self.request)
80-
81- self.context.recalculateCounts()
82-
83- self.pofiles = self.context.getPOFilesFor(
84- self.batchnav.currentBatch())
85- self.parent = self.context.productseries.product
86-
87- @cachedproperty
88- def translation_group(self):
89- return self.context.productseries.product.translationgroup
90-
91- @cachedproperty
92- def translation_team(self):
93- """Is there a translation team for this translation."""
94- if self.translation_group is not None:
95- team = self.translation_group.query_translator(
96- self.context.language)
97- else:
98- team = None
99- return team
100-
101-
102-class ProductSeriesLanguageNavigation(Navigation):
103- """Navigation for `IProductSeriesLanguage`."""
104- usedfor = IProductSeriesLanguage
105
106=== renamed file 'lib/lp/translations/browser/distroserieslanguage.py' => 'lib/lp/translations/browser/serieslanguage.py'
107--- lib/lp/translations/browser/distroserieslanguage.py 2009-09-17 11:10:49 +0000
108+++ lib/lp/translations/browser/serieslanguage.py 2009-12-17 16:29:14 +0000
109@@ -8,38 +8,64 @@
110 __all__ = [
111 'DistroSeriesLanguageNavigation',
112 'DistroSeriesLanguageView',
113+ 'ProductSeriesLanguageNavigation',
114+ 'ProductSeriesLanguageView',
115 ]
116
117+from canonical.cachedproperty import cachedproperty
118 from canonical.launchpad.webapp import LaunchpadView
119+from canonical.launchpad.webapp.tales import PersonFormatterAPI
120 from canonical.launchpad.webapp.batching import BatchNavigator
121 from canonical.launchpad.webapp.publisher import Navigation
122 from lp.translations.interfaces.distroserieslanguage import (
123 IDistroSeriesLanguage)
124-
125-class DistroSeriesLanguageView(LaunchpadView):
126- """View class to render translation status for an `IDistroSeries`."""
127+from lp.translations.interfaces.translationsperson import (
128+ ITranslationsPerson)
129+from lp.translations.interfaces.translationgroup import (
130+ TranslationPermission)
131+from lp.translations.interfaces.productserieslanguage import (
132+ IProductSeriesLanguage)
133+
134+
135+class BaseSeriesLanguageView(LaunchpadView):
136+ """View base class to render translation status for an
137+ `IDistroSeries` and `IProductSeries`
138+
139+ This class should not be directly instantiated.
140+ """
141
142 pofiles = None
143 label = "Translatable templates"
144+ series = None
145+ parent = None
146+ translationgroup = None
147
148- def initialize(self):
149+ def initialize(self, series, translationgroup):
150+ self.series = series
151+ self.translationgroup = translationgroup
152 self.form = self.request.form
153
154 self.batchnav = BatchNavigator(
155- self.context.distroseries.getCurrentTranslationTemplates(),
156+ self.series.getCurrentTranslationTemplates(),
157 self.request)
158
159 self.pofiles = self.context.getPOFilesFor(
160 self.batchnav.currentBatch())
161- self.parent = self.context.distroseries.distribution
162
163 @property
164 def translation_group(self):
165- return self.context.distroseries.distribution.translationgroup
166-
167- @property
168+ """Return the translation group for these translations.
169+
170+ Return None if there's no translation group for them.
171+ """
172+ return self.translationgroup
173+
174+ @cachedproperty
175 def translation_team(self):
176- """Is there a translation team for this translation."""
177+ """Return the translation team for these translations.
178+
179+ Return None if there's no translation team for them.
180+ """
181 if self.translation_group is not None:
182 team = self.translation_group.query_translator(
183 self.context.language)
184@@ -47,7 +73,95 @@
185 team = None
186 return team
187
188+ @property
189+ def access_level_description(self):
190+ """Must not be called when there's no translation group."""
191+
192+ if self.user is None:
193+ return ("You are not logged in. Please log in to work "
194+ "on translations.")
195+
196+ translations_person = ITranslationsPerson(self.user)
197+ translations_contact_link = None
198+
199+ if self.translation_team:
200+ translations_contact_link = PersonFormatterAPI(
201+ self.translation_team.translator).link(None)
202+ elif self.translation_group:
203+ translations_contact_link = PersonFormatterAPI(
204+ self.translation_group.owner).link(None)
205+ else:
206+ assert self.translation_group is not None, (
207+ "Must not be called when there's no translation group.")
208+
209+ if not translations_person.translations_relicensing_agreement:
210+ translation_license_url = PersonFormatterAPI(
211+ self.user).url(
212+ view_name='+licensing',
213+ rootsite='translations')
214+ return ("To make translations in Launchpad you need to "
215+ "agree with the "
216+ "<a href='%s'>Translations licensing</a>.") % (
217+ translation_license_url)
218+
219+ sample_pofile = self.pofiles[0]
220+ if sample_pofile is not None:
221+ if sample_pofile.canEditTranslations(self.user):
222+ return "You can add and review translations."
223+
224+ if sample_pofile.canAddSuggestions(self.user):
225+ return ("Your suggestions will be held for review by "
226+ "%s. If you need help, or your translations are "
227+ "not being reviewed, please get in touch with "
228+ "%s.") % (
229+ translations_contact_link,
230+ translations_contact_link)
231+
232+ permission = sample_pofile.translationpermission
233+ if permission == TranslationPermission.CLOSED:
234+ return ("These templates can be translated only by "
235+ "their managers.")
236+
237+ if self.translation_team is None:
238+ return ("Since there is nobody to manage translation "
239+ "approvals into this language, your cannot add "
240+ "new suggestions. If you are interested in making "
241+ "translations, please contact %s.") % (
242+ translations_contact_link)
243+
244+ raise AssertionError(
245+ "BUG! Couldn't identify the user's access level for these "
246+ "translations.")
247+
248+
249+class DistroSeriesLanguageView(BaseSeriesLanguageView):
250+ """View class to render translation status for an `IDistroSeries`."""
251+
252+ def initialize(self):
253+ series = self.context.distroseries
254+ super(DistroSeriesLanguageView, self).initialize(
255+ series=series,
256+ translationgroup=series.distribution.translationgroup)
257+ self.parent = self.series.distribution
258+
259+
260+class ProductSeriesLanguageView(BaseSeriesLanguageView):
261+ """View class to render translation status for an `IProductSeries`."""
262+
263+ def initialize(self):
264+ series = self.context.productseries
265+ super(ProductSeriesLanguageView, self).initialize(
266+ series=series,
267+ translationgroup=series.product.translationgroup)
268+ self.context.recalculateCounts()
269+ self.parent = self.series.product
270+
271
272 class DistroSeriesLanguageNavigation(Navigation):
273 """Navigation for `IDistroSeriesLanguage`."""
274 usedfor = IDistroSeriesLanguage
275+
276+
277+class ProductSeriesLanguageNavigation(Navigation):
278+ """Navigation for `IProductSeriesLanguage`."""
279+ usedfor = IProductSeriesLanguage
280
281=== modified file 'lib/lp/translations/browser/tests/test_distroserieslanguage_views.py'
282--- lib/lp/translations/browser/tests/test_distroserieslanguage_views.py 2009-09-11 06:57:21 +0000
283+++ lib/lp/translations/browser/tests/test_distroserieslanguage_views.py 2009-12-17 16:29:14 +0000
284@@ -8,7 +8,7 @@
285
286 from zope.component import getUtility
287
288-from lp.translations.browser.distroserieslanguage import (
289+from lp.translations.browser.serieslanguage import (
290 DistroSeriesLanguageView)
291 from lp.translations.interfaces.translator import ITranslatorSet
292 from canonical.launchpad.webapp.servers import LaunchpadTestRequest
293@@ -48,6 +48,9 @@
294 group = self.factory.makeTranslationGroup(
295 self.distroseries.distribution.owner, url=None)
296 self.distroseries.distribution.translationgroup = group
297+ self.view = DistroSeriesLanguageView(
298+ self.dsl, LaunchpadTestRequest())
299+ self.view.initialize()
300 self.assertEquals(self.view.translation_group, group)
301
302 def test_translation_team(self):
303@@ -66,6 +69,7 @@
304 # Recreate the view because we are using a cached property.
305 self.view = DistroSeriesLanguageView(
306 self.dsl, LaunchpadTestRequest())
307+ self.view.initialize()
308 self.assertEquals(self.view.translation_team, translator)
309
310 def test_suite():
311
312=== modified file 'lib/lp/translations/browser/tests/test_productserieslanguage_views.py'
313--- lib/lp/translations/browser/tests/test_productserieslanguage_views.py 2009-07-17 00:26:05 +0000
314+++ lib/lp/translations/browser/tests/test_productserieslanguage_views.py 2009-12-17 16:29:14 +0000
315@@ -7,7 +7,7 @@
316
317 from zope.component import getUtility
318
319-from lp.translations.browser.productserieslanguage import (
320+from lp.translations.browser.serieslanguage import (
321 ProductSeriesLanguageView)
322 from lp.translations.interfaces.translator import ITranslatorSet
323 from canonical.launchpad.webapp.servers import LaunchpadTestRequest
324@@ -137,6 +137,7 @@
325 group = self.factory.makeTranslationGroup(
326 self.productseries.product.owner, url=None)
327 self.productseries.product.translationgroup = group
328+ self.view.initialize()
329 self.assertEquals(self.view.translation_group, group)
330
331 def test_translation_team(self):
332@@ -155,6 +156,7 @@
333 # Recreate the view because we are using a cached property.
334 self.view = ProductSeriesLanguageView(
335 self.psl, LaunchpadTestRequest())
336+ self.view.initialize()
337 self.assertEquals(self.view.translation_team, translator)
338
339 def test_suite():
340
341=== renamed file 'lib/lp/translations/stories/productseries/xx-productserieslanguage.txt' => 'lib/lp/translations/stories/standalone/xx-serieslanguage-index.txt'
342--- lib/lp/translations/stories/productseries/xx-productserieslanguage.txt 2009-09-12 07:25:21 +0000
343+++ lib/lp/translations/stories/standalone/xx-serieslanguage-index.txt 2009-12-17 16:29:14 +0000
344@@ -1,4 +1,9 @@
345-= Product release series language overview =
346+Product or distribution release series language overview
347+========================================================
348+
349+These pages are used by translators for accessing all templates in a
350+release series and viewing translation statistics for each template for
351+a specific language
352
353 The Translations page for a product release series with multiple
354 templates links to per-language overviews.
355@@ -11,4 +16,187 @@
356 >>> print browser.title
357 Portuguese (Brazil) (pt_BR) : Translations : Series trunk : Evolution
358
359+Since there is no translation team to manage Portuguese (Brazil) language
360+in the Evolution's translation group, all users will be informed about it
361+and pointed to the translation group owner.
362+
363+ >>> print extract_text(find_tag_by_id(
364+ ... browser.contents, 'group-team-info'))
365+ There is no team to manage Evolution ... translations to ...
366+ To set one up, please get in touch with Carlos Perelló Marín.
367+
368+Anonymous users are informed that in order to make translations they
369+need to login first.
370+
371+ >>> print extract_text(
372+ ... find_tag_by_id(browser.contents, 'translation-access-level'))
373+ You are not logged in. Please log in to work on translations...
374+
375+Authenticated users will see information about what they can do in
376+these translations. Things like review, only add suggestion or no
377+changes at all.
378+
379+If a product or distribution had no translation group, visitors are
380+informed about this fact and will be able to add translations without
381+requiring a review.
382+
383+ >>> user_browser.open(
384+ ... 'http://translations.launchpad.dev/ubuntu/hoary/+lang/es')
385+ >>> print extract_text(
386+ ... find_tag_by_id(user_browser.contents, 'group-team-info'))
387+ There is no translation group to manage Ubuntu translations.
388+
389+Create a translation group for Ubuntu, together with a translation
390+person for managing Ubuntu Spanish translations and set translation
391+policy to RESTRICTED.
392+This is done to so see what the page will look like when they exist.
393+
394+ >>> from zope.component import getUtility
395+ >>> from canonical.launchpad.interfaces import ILaunchpadCelebrities
396+ >>> from lp.translations.interfaces.translationgroup import (
397+ ... TranslationPermission)
398+ >>> login('foo.bar@canonical.com')
399+ >>> utc_owner = factory.makePerson(displayname='Some Guy')
400+ >>> utc_team = factory.makeTeam(
401+ ... owner=utc_owner, name='utc-team',
402+ ... displayname='Ubuntu Translation Coordinators')
403+ >>> utg = factory.makeTranslationGroup(
404+ ... owner=utc_team, name='utg', title='Ubuntu Translation Group')
405+ >>> st_coordinator = factory.makePerson(
406+ ... name="ubuntu-l10n-es",
407+ ... displayname='Ubuntu Spanish Translators')
408+ >>> dude = factory.makePerson(
409+ ... name="dude", email="dude@ex.com", password="test")
410+ >>> ubuntu = getUtility(ILaunchpadCelebrities).ubuntu
411+ >>> ubuntu.translationgroup = utg
412+ >>> ubuntu.translationpermission = TranslationPermission.RESTRICTED
413+ >>> translators = factory.makeTranslator(
414+ ... 'es', group=utg, person=st_coordinator)
415+ >>> no_license_translator = factory.makeTranslator(
416+ ... 'es', person=dude, license=False)
417+ >>> logout()
418+
419+Spanish has a translation team for managing its translations and all
420+Evolution Spanish templates can be accessed from the distribution series
421+translation page.
422+
423+ >>> user_browser.open(
424+ ... 'http://translations.launchpad.dev/ubuntu/hoary/')
425+ >>> user_browser.getLink('Spanish').click()
426+ >>> print user_browser.url
427+ http://translations.launchpad.dev/ubuntu/hoary/+lang/es
428+
429+ >>> print extract_text(
430+ ... find_tag_by_id(user_browser.contents, 'group-team-info'))
431+ These Ubuntu translations are managed by Ubuntu Spanish Translators.
432+
433+Authenticated users can add suggestion but will be held for review by
434+the members of Spanish translations team.
435+
436+ >>> print extract_text(
437+ ... find_tag_by_id(
438+ ... user_browser.contents, 'translation-access-level'))
439+ Your suggestions will be held for review by
440+ Ubuntu Spanish Translator...
441+ please get in touch with Ubuntu Spanish Translators...
442+
443+Users will see three references to the team managing these translations.
444+
445+ >>> print user_browser.getLink(
446+ ... 'Ubuntu Spanish Translator').url
447+ http://launchpad.dev/~ubuntu-l10n-es
448+
449+Catalan has no translation team for managing translations and since
450+there is no one to review the work, authenticated users can not add
451+suggestions.
452+
453+ >>> user_browser.open(
454+ ... 'http://translations.launchpad.dev/ubuntu/hoary/')
455+ >>> user_browser.getLink('Catalan').click()
456+ >>> print user_browser.url
457+ http://translations.launchpad.dev/ubuntu/hoary/+lang/ca
458+
459+ >>> print extract_text(
460+ ... find_tag_by_id(user_browser.contents, 'group-team-info'))
461+ There is no team to manage ... To set one up, please get in touch
462+ with Ubuntu Translation Coordinators.
463+
464+ >>> print extract_text(find_tag_by_id(
465+ ... user_browser.contents, 'translation-access-level'))
466+ Since there is nobody to manage translation ...
467+ your cannot add new suggestions. If you are interested in making
468+ translations, please contact Ubuntu Translation Coordinators...
469+
470+ >>> print user_browser.getLink(
471+ ... 'Ubuntu Translation Coordinators').url
472+ http://launchpad.dev/~utc-team
473+
474+Members of translation team and translations admins have full access to
475+translations. They can add and review translations.
476+
477+ >>> admin_browser.open(
478+ ... 'http://translations.launchpad.dev/ubuntu/hoary/+lang/ro')
479+ >>> print extract_text(find_tag_by_id(
480+ ... admin_browser.contents, 'translation-access-level'))
481+ You can add and review translations...
482+
483+For projects using closed translations policy, a translator that is not
484+member of the translation team appointed for that language will not
485+be allowed to make any changes.
486+
487+ >>> login('foo.bar@canonical.com')
488+ >>> ubuntu.translationpermission = TranslationPermission.CLOSED
489+ >>> logout()
490+
491+ >>> user_browser.open(
492+ ... 'http://translations.launchpad.dev/ubuntu/hoary/+lang/ro')
493+ >>> print extract_text(find_tag_by_id(
494+ ... user_browser.contents, 'translation-access-level'))
495+ These templates can be translated only by their managers...
496+
497+Translation policy is rolled back to not affect other tests.
498+
499+ >>> login('foo.bar@canonical.com')
500+ >>> ubuntu.translationpermission = TranslationPermission.RESTRICTED
501+ >>> logout()
502+
503+Translators that have not agreed with the license can not make
504+translations, and will see a link to the license page.
505+
506+ >>> no_license_browser = setupBrowser(
507+ ... auth='Basic dude@ex.com:test')
508+ >>> no_license_browser.open(
509+ ... 'http://translations.launchpad.dev/ubuntu/hoary/+lang/ro')
510+ >>> print extract_text(find_tag_by_id(
511+ ... no_license_browser.contents, 'translation-access-level'))
512+ To make translations in Launchpad you need to agree with
513+ the Translations licensing...
514+
515+ >>> print no_license_browser.getLink(
516+ ... 'Translations licensing').url
517+ http://translations.launchpad.dev/~dude/+licensing
518+
519+For project with no translation group, translators see a note stating
520+this fact. No access level information is displayed.
521+
522+ >>> login('foo.bar@canonical.com')
523+ >>> ubuntu.translationgroup = None
524+ >>> logout()
525+
526+ >>> user_browser.open(
527+ ... 'http://translations.launchpad.dev/ubuntu/hoary/+lang/ro')
528+ >>> print extract_text(
529+ ... find_tag_by_id(user_browser.contents, 'group-team-info'))
530+ There is no translation group to manage Ubuntu translations.
531+
532+ >>> print extract_text(find_tag_by_id(
533+ ... user_browser.contents, 'translation-access-level'))
534+ Templates which are more important to translate are listed first.
535+
536+Translation group configuration is rolled back to not affect other tests.
537+
538+ >>> login('foo.bar@canonical.com')
539+ >>> ubuntu.translationgroup = utg
540+ >>> logout()
541+
542 The details of the page are tested at the view level.
543
544=== modified file 'lib/lp/translations/templates/serieslanguage-index.pt'
545--- lib/lp/translations/templates/serieslanguage-index.pt 2009-09-17 11:10:49 +0000
546+++ lib/lp/translations/templates/serieslanguage-index.pt 2009-12-17 16:29:14 +0000
547@@ -16,33 +16,45 @@
548
549 <tal:block condition="view/pofiles">
550 <div class="top-portlet">
551- <tal:with_group condition="view/translation_group">
552- <p tal:condition="view/translation_team">
553- <tal:product replace="structure
554- view/parent/fmt:link">Evolution</tal:product>
555- is translated by
556- <tal:team replace="structure
557- view/translation_team/translator/fmt:link">
558- Serbian translators</tal:team> — if you need help, or your
559- translations are not being reviewed, please get in touch with them.
560- </p>
561-
562- <p tal:condition="not:view/translation_team">
563- There is no team to translate
564- <tal:product replace="structure
565- view/parent/fmt:link">Evolution</tal:product>
566- to
567- <a tal:attributes="href context/language/fmt:url"
568- tal:content="context/language/englishname">Serbian</a>.
569- To set one up, please get in touch with
570- <a tal:replace="structure view/translation_group/fmt:link"
571- >Launchpad Translators</a>
572- coordinator.
573- </p>
574- </tal:with_group>
575-
576- <p>
577- Choose a template name to begin translating.
578+ <div id="group-team-info">
579+ <tal:with_group condition="view/translation_group">
580+ <p tal:condition="view/translation_team">
581+ These
582+ <tal:product replace="structure
583+ view/parent/fmt:link">Evolution</tal:product>
584+ translations are managed by
585+ <tal:team replace="structure
586+ view/translation_team/translator/fmt:link">
587+ Serbian translators</tal:team>.
588+ </p>
589+
590+ <p tal:condition="not:view/translation_team">
591+ There is no team to manage
592+ <tal:product replace="structure
593+ view/parent/fmt:link">Evolution</tal:product>
594+ translations to
595+ <a tal:attributes="href context/language/fmt:url"
596+ tal:content="context/language/englishname">Serbian</a>.
597+ To set one up, please get in touch with
598+ <a tal:replace="structure view/translation_group/owner/fmt:link"
599+ >Launchpad Translators</a>.
600+ </p>
601+ </tal:with_group>
602+ <tal:without_group condition="not: view/translation_group">
603+ <p>
604+ There is no translation group to manage
605+ <tal:product replace="structure
606+ view/parent/fmt:link">Evolution</tal:product>
607+ translations.
608+ </p>
609+ </tal:without_group>
610+ </div>
611+ <p id="translation-access-level">
612+ <span
613+ tal:condition="view/translation_group"
614+ tal:replace="structure view/access_level_description">
615+ You can add suggestions for these translations.
616+ </span>
617 Templates which are more important to translate are listed first.
618 </p>
619 </div>