Merge lp:~launchpad/launchpad/translation-sharing-status into lp:launchpad

Proposed by Henning Eggers
Status: Merged
Approved by: Henning Eggers
Approved revision: no longer in the source branch.
Merged at revision: 12606
Proposed branch: lp:~launchpad/launchpad/translation-sharing-status
Merge into: lp:launchpad
Diff against target: 802 lines (+703/-8)
6 files modified
lib/lp/testing/factory.py (+3/-3)
lib/lp/translations/browser/configure.zcml (+7/-0)
lib/lp/translations/browser/productseries.py (+3/-1)
lib/lp/translations/browser/sourcepackage.py (+132/-4)
lib/lp/translations/browser/tests/test_sharing_details.py (+437/-0)
lib/lp/translations/templates/sourcepackage-sharing-details.pt (+121/-0)
To merge this branch: bzr merge lp:~launchpad/launchpad/translation-sharing-status
Reviewer Review Type Date Requested Status
Curtis Hovey (community) ui Approve
Henning Eggers (community) Abstain
Leonard Richardson (community) Approve
Review via email: mp+53419@code.launchpad.net

Commit message

[r=leonardr][ui=sinzui][bug=732633] Added translation sharing details page.

Description of the change

Details
=======

This branch adds the new +sharing-details page that displays information about the sharing configuration and the current sharing state for each sharing template in a source package.

Here are some screenshots of the page:
http://people.canonical.com/~henninge/screenshots/sharing-details-page-unconfigured.png
http://people.canonical.com/~henninge/screenshots/sharing-details-page-incomplete.png
http://people.canonical.com/~henninge/screenshots/sharing-details-page-notice.png
http://people.canonical.com/~henninge/screenshots/sharing-details-page.png

The first part is a checklist that can be followed to set up upstream translation sharing. It indicates if each step has been completed an provides links to the respective places on Launchpad where these configurations can be performed.

The second part is a table of all templates in both the sourcepackage and the upstream project. The information is useful for maintainers to have an overview if all templates are sharing properly and if any of them may need updating.

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

We (adeuring, heninge) worked on this branch together, so it is owned by ~launchpad. Will be interesting to see how that works through our merge machinery ...

The edit links in the checklist use menu items from the upstream product series. Thus the icon as well as the permissions are as they are defined in that menu. One item did not have an icon, so this was added.

The rest of the implementation is pretty straight forward: view class, template, page test. The view is hidden behind a feature flag, it will raise NotFound if the flag is not set.

Test
----

bin/test -vvvcm lp.translations.browser.tests.test_sharing_details

To post a comment you must log in.
Revision history for this message
Leonard Richardson (leonardr) wrote :

I only have minor suggestions. I was able to follow the logic of the code, but I don't have a good grasp of how these parts of Launchpad (translation templates, source packages, distro series) work together, so I may have missed something. Feel free to ask for another review.

* You have some copy-and-paste comments on lines 398, 405, and 413.
* On line 543, the docstring reads like a commit message. you should say what you're actually setting up. You may also want to refactor your calls to getViewBrowser, but that's not a big deal.
* User-visible typo on line 753, "Translations are enable" -> "Translations are enabled"

review: Approve
Revision history for this message
Curtis Hovey (sinzui) wrote :

Hi Henning.

The think the configuration progress is good. I am surprised by the table headings though. Why are the subordinate headings a different colour? Is this because some can be sorted?

review: Needs Fixing (ui)
Revision history for this message
Curtis Hovey (sinzui) wrote :

Sorry Henning.

I do not think you need to change anything. I just asked a question.

review: Needs Information (ui)
Revision history for this message
Henning Eggers (henninge) wrote :

Thank you for your review! I fixed the issues and also put the getViewBrowser
repetitions in a method. That was something I had already been considering.

=== modified file 'lib/lp/translations/browser/tests/test_sharing_details.py'
--- lib/lp/translations/browser/tests/test_sharing_details.py 2011-03-15 10:48:28 +0000
+++ lib/lp/translations/browser/tests/test_sharing_details.py 2011-03-15 18:58:11 +0000
@@ -156,14 +156,14 @@
156 self.assertFalse(self.view.is_upstream_synchronization_enabled)156 self.assertFalse(self.view.is_upstream_synchronization_enabled)
157157
158 def test_is_upstream_synchronization_enabled__no_import(self):158 def test_is_upstream_synchronization_enabled__no_import(self):
159 # If the source package is not linked to an upstream series,159 # If no synchronization is enabled on the upstream series,
160 # is_upstream_synchronization_enabled returns False.160 # is_upstream_synchronization_enabled returns False.
161 self.configureSharing(161 self.configureSharing(
162 translation_import_mode=TranslationsBranchImportMode.NO_IMPORT)162 translation_import_mode=TranslationsBranchImportMode.NO_IMPORT)
163 self.assertFalse(self.view.is_upstream_synchronization_enabled)163 self.assertFalse(self.view.is_upstream_synchronization_enabled)
164164
165 def test_is_upstream_synchronization_enabled__import_templates(self):165 def test_is_upstream_synchronization_enabled__import_templates(self):
166 # If the source package is not linked to an upstream series,166 # If only template synchronization is enabled on the upstream series,
167 # is_upstream_synchronization_enabled returns False.167 # is_upstream_synchronization_enabled returns False.
168 self.configureSharing(168 self.configureSharing(
169 translation_import_mode=169 translation_import_mode=
@@ -171,8 +171,8 @@
171 self.assertFalse(self.view.is_upstream_synchronization_enabled)171 self.assertFalse(self.view.is_upstream_synchronization_enabled)
172172
173 def test_is_upstream_synchronization_enabled__import_translations(self):173 def test_is_upstream_synchronization_enabled__import_translations(self):
174 # If the source package is not linked to an upstream series,174 # If full translation synchronization is enabled on the upstream
175 # is_upstream_synchronization_enabled returns False.175 # series, is_upstream_synchronization_enabled returns False.
176 self.configureSharing(176 self.configureSharing(
177 translation_import_mode=177 translation_import_mode=
178 TranslationsBranchImportMode.IMPORT_TRANSLATIONS)178 TranslationsBranchImportMode.IMPORT_TRANSLATIONS)
@@ -301,7 +301,7 @@
301 return self.factory.makeSourcePackage(distroseries=distroseries)301 return self.factory.makeSourcePackage(distroseries=distroseries)
302302
303 def _makeFullyConfiguredSharing(self):303 def _makeFullyConfiguredSharing(self):
304 """Remove some redundant code from the tests."""304 """Setup a fully configured sharing scenario."""
305 packaging = self.factory.makePackagingLink(in_ubuntu=True)305 packaging = self.factory.makePackagingLink(in_ubuntu=True)
306 productseries = packaging.productseries306 productseries = packaging.productseries
307 sourcepackage = packaging.sourcepackage307 sourcepackage = packaging.sourcepackage
@@ -313,12 +313,15 @@
313 TranslationsBranchImportMode.IMPORT_TRANSLATIONS))313 TranslationsBranchImportMode.IMPORT_TRANSLATIONS))
314 return (sourcepackage, productseries)314 return (sourcepackage, productseries)
315315
316 def _getSharingDetailsViewBrowser(self, sourcepackage):
317 return self.getViewBrowser(
318 sourcepackage, no_login=True, rootsite="translations",
319 view_name="+sharing-details")
320
316 def test_checklist_unconfigured(self):321 def test_checklist_unconfigured(self):
317 # Without a packaging link, sharing is completely unconfigured322 # Without a packaging link, sharing is completely unconfigured
318 sourcepackage = self._makeSourcePackage()323 sourcepackage = self._makeSourcePackage()
319 browser = self.getViewBrowser(324 browser = self._getSharingDetailsViewBrowser(sourcepackage)
320 sourcepackage, no_login=True, rootsite="translations",
321 view_name="+sharing-details")
322 checklist = find_tag_by_id(browser.contents, 'sharing-checklist')325 checklist = find_tag_by_id(browser.contents, 'sharing-checklist')
323 self.assertIsNot(None, checklist)326 self.assertIsNot(None, checklist)
324 self.assertTextMatchesExpressionIgnoreWhitespace("""327 self.assertTextMatchesExpressionIgnoreWhitespace("""
@@ -332,9 +335,7 @@
332 def test_checklist_partly_configured(self):335 def test_checklist_partly_configured(self):
333 # Linking a source package takes care of one item.336 # Linking a source package takes care of one item.
334 packaging = self.factory.makePackagingLink(in_ubuntu=True)337 packaging = self.factory.makePackagingLink(in_ubuntu=True)
335 browser = self.getViewBrowser(338 browser = self._getSharingDetailsViewBrowser(packaging.sourcepackage)
336 packaging.sourcepackage, no_login=True, rootsite="translations",
337 view_name="+sharing-details")
338 checklist = find_tag_by_id(browser.contents, 'sharing-checklist')339 checklist = find_tag_by_id(browser.contents, 'sharing-checklist')
339 self.assertIsNot(None, checklist)340 self.assertIsNot(None, checklist)
340 self.assertTextMatchesExpressionIgnoreWhitespace("""341 self.assertTextMatchesExpressionIgnoreWhitespace("""
@@ -349,9 +350,7 @@
349 def test_checklist_fully_configured(self):350 def test_checklist_fully_configured(self):
350 # A fully configured sharing setup.351 # A fully configured sharing setup.
351 sourcepackage = self._makeFullyConfiguredSharing()[0]352 sourcepackage = self._makeFullyConfiguredSharing()[0]
352 browser = self.getViewBrowser(353 browser = self._getSharingDetailsViewBrowser(sourcepackage)
353 sourcepackage, no_login=True, rootsite="translations",
354 view_name="+sharing-details")
355 checklist = find_tag_by_id(browser.contents, 'sharing-checklist')354 checklist = find_tag_by_id(browser.contents, 'sharing-checklist')
356 self.assertIsNot(None, checklist)355 self.assertIsNot(None, checklist)
357 self.assertTextMatchesExpressionIgnoreWhitespace("""356 self.assertTextMatchesExpressionIgnoreWhitespace("""
@@ -359,7 +358,7 @@
359 Linked upstream series is .+ trunk series.358 Linked upstream series is .+ trunk series.
360 Change upstream link Remove upstream link359 Change upstream link Remove upstream link
361 Upstream source branch is .+[.]360 Upstream source branch is .+[.]
362 Translations are enable on the upstream project.361 Translations are enabled on the upstream project.
363 Automatic synchronization of translations is enabled.""",362 Automatic synchronization of translations is enabled.""",
364 extract_text(checklist))363 extract_text(checklist))
365364
@@ -368,9 +367,7 @@
368 sourcepackage = self._makeSourcePackage()367 sourcepackage = self._makeSourcePackage()
369 self.factory.makePOTemplate(368 self.factory.makePOTemplate(
370 name='foo-template', sourcepackage=sourcepackage)369 name='foo-template', sourcepackage=sourcepackage)
371 browser = self.getViewBrowser(370 browser = self._getSharingDetailsViewBrowser(sourcepackage)
372 sourcepackage, no_login=True, rootsite="translations",
373 view_name="+sharing-details")
374 tbody = find_tag_by_id(371 tbody = find_tag_by_id(
375 browser.contents, 'template-table').find('tbody')372 browser.contents, 'template-table').find('tbody')
376 self.assertIsNot(None, tbody)373 self.assertIsNot(None, tbody)
@@ -386,9 +383,7 @@
386 name=template_name, sourcepackage=sourcepackage)383 name=template_name, sourcepackage=sourcepackage)
387 self.factory.makePOTemplate(384 self.factory.makePOTemplate(
388 name=template_name, productseries=productseries)385 name=template_name, productseries=productseries)
389 browser = self.getViewBrowser(386 browser = self._getSharingDetailsViewBrowser(sourcepackage)
390 sourcepackage, no_login=True, rootsite="translations",
391 view_name="+sharing-details")
392 tbody = find_tag_by_id(387 tbody = find_tag_by_id(
393 browser.contents, 'template-table').find('tbody')388 browser.contents, 'template-table').find('tbody')
394 self.assertIsNot(None, tbody)389 self.assertIsNot(None, tbody)
@@ -404,9 +399,7 @@
404 template_name = 'foo-template'399 template_name = 'foo-template'
405 self.factory.makePOTemplate(400 self.factory.makePOTemplate(
406 name=template_name, productseries=productseries)401 name=template_name, productseries=productseries)
407 browser = self.getViewBrowser(402 browser = self._getSharingDetailsViewBrowser(sourcepackage)
408 sourcepackage, no_login=True, rootsite="translations",
409 view_name="+sharing-details")
410 tbody = find_tag_by_id(403 tbody = find_tag_by_id(
411 browser.contents, 'template-table').find('tbody')404 browser.contents, 'template-table').find('tbody')
412 self.assertIsNot(None, tbody)405 self.assertIsNot(None, tbody)
@@ -418,9 +411,7 @@
418 # When sharing is fully configured but no upstream templates are411 # When sharing is fully configured but no upstream templates are
419 # found, a message is displayed.412 # found, a message is displayed.
420 sourcepackage = self._makeFullyConfiguredSharing()[0]413 sourcepackage = self._makeFullyConfiguredSharing()[0]
421 browser = self.getViewBrowser(414 browser = self._getSharingDetailsViewBrowser(sourcepackage)
422 sourcepackage, no_login=True, rootsite="translations",
423 view_name="+sharing-details")
424 self.assertEqual(415 self.assertEqual(
425 ["No upstream templates have been found yet. Please follow "416 ["No upstream templates have been found yet. Please follow "
426 "the import process by going to the Translation Import Queue "417 "the import process by going to the Translation Import Queue "
@@ -432,9 +423,7 @@
432 # message should be displayed.423 # message should be displayed.
433 sourcepackage, productseries = self._makeFullyConfiguredSharing()424 sourcepackage, productseries = self._makeFullyConfiguredSharing()
434 self.factory.makePOTemplate(productseries=productseries)425 self.factory.makePOTemplate(productseries=productseries)
435 browser = self.getViewBrowser(426 browser = self._getSharingDetailsViewBrowser(sourcepackage)
436 sourcepackage, no_login=True, rootsite="translations",
437 view_name="+sharing-details")
438 self.assertEqual([], get_feedback_messages(browser.contents))427 self.assertEqual([], get_feedback_messages(browser.contents))
439428
440 def test_no_message_with_incomplate_sharing(self):429 def test_no_message_with_incomplate_sharing(self):
@@ -444,7 +433,5 @@
444 productseries = packaging.productseries433 productseries = packaging.productseries
445 sourcepackage = packaging.sourcepackage434 sourcepackage = packaging.sourcepackage
446 self.factory.makePOTemplate(productseries=productseries)435 self.factory.makePOTemplate(productseries=productseries)
447 browser = self.getViewBrowser(436 browser = self._getSharingDetailsViewBrowser(sourcepackage)
448 sourcepackage, no_login=True, rootsite="translations",
449 view_name="+sharing-details")
450 self.assertEqual([], get_feedback_messages(browser.contents))437 self.assertEqual([], get_feedback_messages(browser.contents))
451438
=== modified file 'lib/lp/translations/templates/sourcepackage-sharing-details.pt'
--- lib/lp/translations/templates/sourcepackage-sharing-details.pt 2011-03-14 15:43:14 +0000
+++ lib/lp/translations/templates/sourcepackage-sharing-details.pt 2011-03-15 18:45:49 +0000
@@ -56,7 +56,7 @@
56 </li>56 </li>
57 <li class="sprite yes"57 <li class="sprite yes"
58 tal:condition="view/is_upstream_translations_enabled">58 tal:condition="view/is_upstream_translations_enabled">
59 Translations are enable on the upstream project.59 Translations are enabled on the upstream project.
60 <a tal:condition="view/is_packaging_configured"60 <a tal:condition="view/is_packaging_configured"
61 tal:replace="structure context/productseries/product/menu:translations/settings/fmt:icon" />61 tal:replace="structure context/productseries/product/menu:translations/settings/fmt:icon" />
62 </li>62 </li>
Revision history for this message
Henning Eggers (henninge) wrote :

> The think the configuration progress is good. I am surprised by the table
> headings though. Why are the subordinate headings a different colour? Is this
> because some can be sorted?

Yes, the color difference is introduced by the "sortable" class but that does not play well with subordinate headings anyway, so I removed it. All headings are black now.

Revision history for this message
Henning Eggers (henninge) :
review: Abstain
Revision history for this message
Curtis Hovey (sinzui) wrote :

Thank you Henning.

I think this is good to land.

review: Approve (ui)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py 2011-03-11 04:11:40 +0000
+++ lib/lp/testing/factory.py 2011-03-15 19:18:37 +0000
@@ -1080,10 +1080,10 @@
1080 if sourcepackagename is None or isinstance(sourcepackagename, str):1080 if sourcepackagename is None or isinstance(sourcepackagename, str):
1081 sourcepackagename = self.makeSourcePackageName(sourcepackagename)1081 sourcepackagename = self.makeSourcePackageName(sourcepackagename)
1082 if distroseries is None:1082 if distroseries is None:
1083 distribution = None
1084 if in_ubuntu:1083 if in_ubuntu:
1085 distribution = getUtility(ILaunchpadCelebrities).ubuntu1084 distroseries = self.makeUbuntuDistroSeries()
1086 distroseries = self.makeDistroSeries(distribution=distribution)1085 else:
1086 distroseries = self.makeDistroSeries()
1087 if packaging_type is None:1087 if packaging_type is None:
1088 packaging_type = PackagingType.PRIME1088 packaging_type = PackagingType.PRIME
1089 if owner is None:1089 if owner is None:
10901090
=== modified file 'lib/lp/translations/browser/configure.zcml'
--- lib/lp/translations/browser/configure.zcml 2011-02-14 04:19:37 +0000
+++ lib/lp/translations/browser/configure.zcml 2011-03-15 19:18:37 +0000
@@ -605,6 +605,13 @@
605 for="lp.registry.interfaces.sourcepackage.ISourcePackage"605 for="lp.registry.interfaces.sourcepackage.ISourcePackage"
606 class="lp.translations.browser.translations.TranslationsRedirectView"606 class="lp.translations.browser.translations.TranslationsRedirectView"
607 permission="zope.Public"/>607 permission="zope.Public"/>
608 <browser:page
609 for="lp.registry.interfaces.sourcepackage.ISourcePackage"
610 name="+sharing-details"
611 class="lp.translations.browser.sourcepackage.SourcePackageTranslationSharingDetailsView"
612 permission="zope.Public"
613 template="../templates/sourcepackage-sharing-details.pt"
614 layer="lp.translations.publisher.TranslationsLayer"/>
608 <browser:pages615 <browser:pages
609 for="lp.registry.interfaces.sourcepackage.ISourcePackage"616 for="lp.registry.interfaces.sourcepackage.ISourcePackage"
610 permission="zope.Public"617 permission="zope.Public"
611618
=== modified file 'lib/lp/translations/browser/productseries.py'
--- lib/lp/translations/browser/productseries.py 2011-03-02 17:49:15 +0000
+++ lib/lp/translations/browser/productseries.py 2011-03-15 19:18:37 +0000
@@ -85,7 +85,9 @@
85 @enabled_with_permission('launchpad.Edit')85 @enabled_with_permission('launchpad.Edit')
86 def settings(self):86 def settings(self):
87 """Return a link to configure the translations settings."""87 """Return a link to configure the translations settings."""
88 return Link('+translations-settings', 'Settings', site='translations')88 return Link(
89 '+translations-settings', 'Settings',
90 site='translations', icon='edit')
8991
90 @enabled_with_permission('launchpad.Edit')92 @enabled_with_permission('launchpad.Edit')
91 def requestbzrimport(self):93 def requestbzrimport(self):
9294
=== modified file 'lib/lp/translations/browser/sourcepackage.py'
--- lib/lp/translations/browser/sourcepackage.py 2011-03-08 09:59:36 +0000
+++ lib/lp/translations/browser/sourcepackage.py 2011-03-15 19:18:37 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009 Canonical Ltd. This software is licensed under the1# Copyright 2011 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Browser views for translation pages for sourcepackages."""4"""Browser views for translation pages for sourcepackages."""
@@ -8,8 +8,11 @@
8__all__ = [8__all__ = [
9 'SourcePackageTranslationsExportView',9 'SourcePackageTranslationsExportView',
10 'SourcePackageTranslationsView',10 'SourcePackageTranslationsView',
11 'SourcePackageTranslationSharingStatus',
11 ]12 ]
1213
14from zope.publisher.interfaces import NotFound
15
13from canonical.launchpad.webapp import (16from canonical.launchpad.webapp import (
14 canonical_url,17 canonical_url,
15 enabled_with_permission,18 enabled_with_permission,
@@ -17,19 +20,33 @@
17 NavigationMenu,20 NavigationMenu,
18 )21 )
19from canonical.launchpad.webapp.authorization import check_permission22from canonical.launchpad.webapp.authorization import check_permission
23from canonical.launchpad.webapp.menu import structured
24from canonical.launchpad.webapp.publisher import LaunchpadView
25from lp.app.enums import ServiceUsage
20from lp.registry.interfaces.sourcepackage import ISourcePackage26from lp.registry.interfaces.sourcepackage import ISourcePackage
27from lp.services.features import getFeatureFlag
21from lp.translations.browser.poexportrequest import BaseExportView28from lp.translations.browser.poexportrequest import BaseExportView
22from lp.translations.browser.translations import TranslationsMixin29from lp.translations.browser.translations import TranslationsMixin
23from lp.translations.browser.translationsharing import (30from lp.translations.browser.translationsharing import (
24 TranslationSharingDetailsMixin,31 TranslationSharingDetailsMixin,
25 )32 )
33from lp.translations.interfaces.translations import (
34 TranslationsBranchImportMode,
35 )
26from lp.translations.utilities.translationsharinginfo import (36from lp.translations.utilities.translationsharinginfo import (
27 has_upstream_template,37 has_upstream_template,
28 get_upstream_sharing_info,38 get_upstream_sharing_info,
29 )39 )
3040
3141
42class SharingDetailsPermissionsMixin:
43
44 def can_edit_sharing_details(self):
45 return check_permission('launchpad.Edit', self.context.distroseries)
46
47
32class SourcePackageTranslationsView(TranslationsMixin,48class SourcePackageTranslationsView(TranslationsMixin,
49 SharingDetailsPermissionsMixin,
33 TranslationSharingDetailsMixin):50 TranslationSharingDetailsMixin):
3451
35 @property52 @property
@@ -56,9 +73,6 @@
56 """See `TranslationSharingDetailsMixin`."""73 """See `TranslationSharingDetailsMixin`."""
57 return self.context74 return self.context
5875
59 def can_edit_sharing_details(self):
60 return check_permission('launchpad.Edit', self.context.distroseries)
61
6276
63class SourcePackageTranslationsMenu(NavigationMenu):77class SourcePackageTranslationsMenu(NavigationMenu):
64 usedfor = ISourcePackage78 usedfor = ISourcePackage
@@ -100,3 +114,117 @@
100 @property114 @property
101 def label(self):115 def label(self):
102 return "Download translations for %s" % self.download_description116 return "Download translations for %s" % self.download_description
117
118
119class SourcePackageTranslationSharingDetailsView(
120 LaunchpadView,
121 SharingDetailsPermissionsMixin):
122 """Details about translation sharing."""
123
124 page_title = "Sharing details"
125
126 def initialize(self):
127 if not getFeatureFlag('translations.sharing_information.enabled'):
128 raise NotFound(self.context, '+sharing-details')
129 super(SourcePackageTranslationSharingDetailsView, self).initialize()
130 has_no_upstream_templates = (
131 self.is_configuration_complete and
132 not has_upstream_template(self.context))
133 if has_no_upstream_templates:
134 self.request.response.addInfoNotification(
135 structured(
136 'No upstream templates have been found yet. Please follow '
137 'the import process by going to the '
138 '<a href="%s">Translation Import Queue</a> of the '
139 'upstream project series.' %(
140 canonical_url(
141 self.context.productseries, rootsite='translations',
142 view_name="+imports"))))
143
144 @property
145 def is_packaging_configured(self):
146 """Is a packaging link defined for this branch?"""
147 return self.context.direct_packaging is not None
148
149 @property
150 def no_item_class(self):
151 """CSS class for 'no' items."""
152 css_class = "sprite no"
153 if self.is_packaging_configured:
154 return css_class
155 else:
156 return css_class + " lowlight"
157
158 @property
159 def has_upstream_branch(self):
160 """Does the upstream series have a source code branch?"""
161 if not self.is_packaging_configured:
162 return False
163 return self.context.direct_packaging.productseries.branch is not None
164
165 @property
166 def is_upstream_translations_enabled(self):
167 """Are Launchpad translations enabled for the upstream series?"""
168 if not self.is_packaging_configured:
169 return False
170 product = self.context.direct_packaging.productseries.product
171 return product.translations_usage in (
172 ServiceUsage.LAUNCHPAD, ServiceUsage.EXTERNAL)
173
174 @property
175 def is_upstream_synchronization_enabled(self):
176 """Is automatic synchronization of upstream translations enabled?"""
177 if not self.is_packaging_configured:
178 return False
179 series = self.context.direct_packaging.productseries
180 return (
181 series.translations_autoimport_mode ==
182 TranslationsBranchImportMode.IMPORT_TRANSLATIONS)
183
184 @property
185 def is_configuration_complete(self):
186 """Is anything missing in the set up for translation sharing?"""
187 # A check if the required packaging link exists is implicitly
188 # done in the implementation of the other properties.
189 return (
190 self.has_upstream_branch and
191 self.is_upstream_translations_enabled and
192 self.is_upstream_synchronization_enabled)
193
194 def template_info(self):
195 """Details about translation templates.
196
197 :return: A list of dictionaries containing details about each
198 template. Each dictionary contains:
199 'name': The name of the template
200 'package_template': The package template (may be None)
201 'upstream_template': The corresponding upstream template
202 (may be None)
203 'status': one of the string 'linking', 'shared',
204 'only in Ubuntu', 'only in upstream'
205 """
206 info = {}
207 templates_on_this_side = self.context.getCurrentTranslationTemplates()
208 for template in templates_on_this_side:
209 info[template.name] = {
210 'name': template.name,
211 'package_template': template,
212 'upstream_template': None,
213 'status': 'only in Ubuntu',
214 }
215 if self.is_configuration_complete:
216 upstream_templates = (
217 self.context.productseries.getCurrentTranslationTemplates())
218 for template in upstream_templates:
219 if template.name in info:
220 info[template.name]['upstream_template'] = template
221 info[template.name]['status'] = 'shared'
222 else:
223 info[template.name] = {
224 'name': template.name,
225 'package_template': None,
226 'upstream_template': template,
227 'status': 'only in upstream',
228 }
229 info = info.values()
230 return sorted(info, key=lambda template: template['name'])
103231
=== added file 'lib/lp/translations/browser/tests/test_sharing_details.py'
--- lib/lp/translations/browser/tests/test_sharing_details.py 1970-01-01 00:00:00 +0000
+++ lib/lp/translations/browser/tests/test_sharing_details.py 2011-03-15 19:18:37 +0000
@@ -0,0 +1,437 @@
1# Copyright 2011 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4__metaclass__ = type
5
6from canonical.launchpad.testing.pages import (
7 extract_text,
8 find_tag_by_id,
9 get_feedback_messages,
10 )
11from canonical.launchpad.webapp.servers import LaunchpadTestRequest
12from canonical.testing.layers import (
13 DatabaseFunctionalLayer,
14 )
15from lp.app.enums import ServiceUsage
16from lp.services.features.testing import FeatureFixture
17from lp.testing import (
18 BrowserTestCase,
19 person_logged_in,
20 TestCaseWithFactory,
21 )
22from lp.translations.browser.sourcepackage import (
23 SourcePackageTranslationSharingDetailsView,
24 )
25from lp.translations.interfaces.translations import (
26 TranslationsBranchImportMode,
27 )
28
29
30class ConfigureUpstreamProjectMixin:
31 """Provide a method for project configuration."""
32
33 def configureUpstreamProject(self, productseries,
34 set_upstream_branch=False,
35 translations_usage=ServiceUsage.UNKNOWN,
36 translation_import_mode=TranslationsBranchImportMode.NO_IMPORT):
37 """Configure the productseries and its product as an upstream project.
38 """
39 with person_logged_in(productseries.product.owner):
40 if set_upstream_branch:
41 productseries.branch = self.factory.makeBranch(
42 product=productseries.product)
43 productseries.product.translations_usage = translations_usage
44 productseries.translations_autoimport_mode = (
45 translation_import_mode)
46
47
48class TestSourcePackageTranslationSharingDetailsView(TestCaseWithFactory,
49 ConfigureUpstreamProjectMixin):
50 """Tests for SourcePackageTranslationSharingStatus."""
51
52 layer = DatabaseFunctionalLayer
53
54 def setUp(self):
55 super(TestSourcePackageTranslationSharingDetailsView, self).setUp()
56 self.useFixture(FeatureFixture(
57 {'translations.sharing_information.enabled': 'on'}))
58 distroseries = self.factory.makeUbuntuDistroSeries()
59 self.sourcepackage = self.factory.makeSourcePackage(
60 distroseries=distroseries)
61 self.ubuntu_only_template = self.factory.makePOTemplate(
62 sourcepackage=self.sourcepackage, name='ubuntu-only')
63 self.shared_template_ubuntu_side = self.factory.makePOTemplate(
64 sourcepackage=self.sourcepackage, name='shared-template')
65 self.productseries = self.factory.makeProductSeries()
66 self.shared_template_upstream_side = self.factory.makePOTemplate(
67 productseries=self.productseries, name='shared-template')
68 self.upstream_only_template = self.factory.makePOTemplate(
69 productseries=self.productseries, name='upstream-only')
70 self.view = SourcePackageTranslationSharingDetailsView(
71 self.sourcepackage, LaunchpadTestRequest())
72 self.view.initialize()
73
74 def configureSharing(self,
75 set_upstream_branch=False,
76 translations_usage=ServiceUsage.UNKNOWN,
77 translation_import_mode=TranslationsBranchImportMode.NO_IMPORT):
78 """Configure translation sharing, at least partially.
79
80 A packaging link is always set; the remaining configuration is
81 done only if explicitly specified.
82 """
83 self.sourcepackage.setPackaging(
84 self.productseries, self.productseries.owner)
85 self.configureUpstreamProject(
86 self.productseries, set_upstream_branch, translations_usage,
87 translation_import_mode)
88
89 def test_is_packaging_configured__not_configured(self):
90 # If a sourcepackage is not linked to a product series,
91 # SourcePackageTranslationSharingStatus.is_packaging_configured
92 # returns False.
93 self.assertFalse(self.view.is_packaging_configured)
94
95 def test_is_packaging_configured__configured(self):
96 # If a sourcepackage is linked to a product series,
97 # SourcePackageTranslationSharingStatus.is_packaging_configured
98 # returns True.
99 self.configureSharing()
100 self.assertTrue(self.view.is_packaging_configured)
101
102 def test_has_upstream_branch__no_packaging_link(self):
103 # If the source package is not linked to an upstream series,
104 # SourcePackageTranslationSharingStatus.has_upstream_branch
105 # returns False.
106 self.assertFalse(self.view.has_upstream_branch)
107
108 def test_has_upstream_branch__no_branch_exists(self):
109 # If the upstream product series does not have any source
110 # code branch,
111 # SourcePackageTranslationSharingStatus.has_upstream_branch
112 # returns False.
113 self.configureSharing()
114 self.assertFalse(self.view.has_upstream_branch)
115
116 def test_has_upstream_branch__branch_exists(self):
117 # If the upstream product series has at least one source
118 # code branch,
119 # SourcePackageTranslationSharingStatus.has_upstream_branch
120 # returns True.
121 self.configureSharing(set_upstream_branch=True)
122 self.assertTrue(self.view.has_upstream_branch)
123
124 def test_is_upstream_translations_enabled__no_packaging_link(self):
125 # If the source package is not linked to an upstream series,
126 # is_upstream_translations_enabled returns False.
127 self.assertFalse(self.view.is_upstream_translations_enabled)
128
129 def test_is_upstream_translations_enabled__when_unknown(self):
130 # If it is unknown what the upstream project uses for
131 # translations, is_upstream_translations_enabled returns False.
132 self.configureSharing(translations_usage=ServiceUsage.UNKNOWN)
133 self.assertFalse(self.view.is_upstream_translations_enabled)
134
135 def test_is_upstream_translations_enabled__when_launchpad(self):
136 # If the upstream product series uses Launchpad for
137 # translations, is_upstream_translations_enabled returns True.
138 self.configureSharing(translations_usage=ServiceUsage.LAUNCHPAD)
139 self.assertTrue(self.view.is_upstream_translations_enabled)
140
141 def test_is_upstream_translations_enabled__when_external(self):
142 # If the upstream product series uses an external tool for
143 # translations, is_upstream_translations_enabled returns True.
144 self.configureSharing(translations_usage=ServiceUsage.EXTERNAL)
145 self.assertTrue(self.view.is_upstream_translations_enabled)
146
147 def test_is_upstream_translations_enabled__when_not_applicable(self):
148 # If the upstream product series does not do translations at all,
149 # is_upstream_translations_enabled returns False.
150 self.configureSharing(translations_usage=ServiceUsage.NOT_APPLICABLE)
151 self.assertFalse(self.view.is_upstream_translations_enabled)
152
153 def test_is_upstream_synchronization_enabled__no_packaging_link(self):
154 # If the source package is not linked to an upstream series,
155 # is_upstream_synchronization_enabled returns False.
156 self.assertFalse(self.view.is_upstream_synchronization_enabled)
157
158 def test_is_upstream_synchronization_enabled__no_import(self):
159 # If no synchronization is enabled on the upstream series,
160 # is_upstream_synchronization_enabled returns False.
161 self.configureSharing(
162 translation_import_mode=TranslationsBranchImportMode.NO_IMPORT)
163 self.assertFalse(self.view.is_upstream_synchronization_enabled)
164
165 def test_is_upstream_synchronization_enabled__import_templates(self):
166 # If only template synchronization is enabled on the upstream series,
167 # is_upstream_synchronization_enabled returns False.
168 self.configureSharing(
169 translation_import_mode=
170 TranslationsBranchImportMode.IMPORT_TEMPLATES)
171 self.assertFalse(self.view.is_upstream_synchronization_enabled)
172
173 def test_is_upstream_synchronization_enabled__import_translations(self):
174 # If full translation synchronization is enabled on the upstream
175 # series, is_upstream_synchronization_enabled returns False.
176 self.configureSharing(
177 translation_import_mode=
178 TranslationsBranchImportMode.IMPORT_TRANSLATIONS)
179 self.assertTrue(self.view.is_upstream_synchronization_enabled)
180
181 def test_is_configuration_complete__nothing_configured(self):
182 # If none of the conditions for translation sharing are
183 # fulfilled (the default test setup), is_configuration_complete
184 # is False.
185 self.assertFalse(self.view.is_configuration_complete)
186
187 def test_is_configuration_complete__only_packaging_set(self):
188 # If the packaging link is set but the other conditions for
189 # translation sharing are not fulfilled, is_configuration_complete
190 # is False.
191 self.configureSharing()
192 self.assertFalse(self.view.is_configuration_complete)
193
194 def test_is_configuration_complete__packaging_upstream_branch_set(self):
195 # If the packaging link is set and if an upstream branch is
196 # configuerd but if the other conditions are not fulfilled,
197 # is_configuration_complete is False.
198 self.configureSharing(set_upstream_branch=True)
199 self.assertFalse(self.view.is_configuration_complete)
200
201 def test_is_configuration_complete__packaging_transl_enabled(self):
202 # If the packaging link is set and if an upstream series
203 # uses Launchpad translations but if the other conditions
204 # are not fulfilled, is_configuration_complete is False.
205 self.configureSharing(translations_usage=ServiceUsage.LAUNCHPAD)
206 self.assertFalse(self.view.is_configuration_complete)
207
208 def test_is_configuration_complete__no_auto_sync(self):
209 # If
210 # - a packaging link is set
211 # - a branch is set for the upstream series
212 # - the upstream series uses Launchpad translations
213 # but if the upstream series does not synchronize translations
214 # then is_configuration_complete is False.
215 self.configureSharing(
216 set_upstream_branch=True,
217 translations_usage=ServiceUsage.LAUNCHPAD)
218 self.assertFalse(self.view.is_configuration_complete)
219
220 def test_is_configuration_complete__all_conditions_fulfilled(self):
221 # If
222 # - a packaging link is set
223 # - a branch is set for the upstream series
224 # - the upstream series uses Launchpad translations
225 # - the upstream series synchronizes translations
226 # then is_configuration_complete is True.
227 self.configureSharing(
228 set_upstream_branch=True,
229 translations_usage=ServiceUsage.LAUNCHPAD,
230 translation_import_mode=
231 TranslationsBranchImportMode.IMPORT_TRANSLATIONS)
232 self.assertTrue(self.view.is_configuration_complete)
233
234 def test_template_info__no_sharing(self):
235 # If translation sharing is not configured,
236 # SourcePackageTranslationSharingDetailsView.info returns
237 # only data about templates in Ubuntu.
238 expected = [
239 {
240 'name': 'shared-template',
241 'status': 'only in Ubuntu',
242 'package_template': self.shared_template_ubuntu_side,
243 'upstream_template': None,
244 },
245 {
246 'name': 'ubuntu-only',
247 'status': 'only in Ubuntu',
248 'package_template': self.ubuntu_only_template,
249 'upstream_template': None,
250 },
251 ]
252 self.assertEqual(expected, self.view.template_info())
253
254 def test_template_info___sharing(self):
255 # If translation sharing is configured,
256 # SourcePackageTranslationSharingDetailsView.info returns
257 # only data about templates in Ubuntu and about upstream
258 # templates.
259 self.configureSharing(
260 set_upstream_branch=True,
261 translations_usage=ServiceUsage.LAUNCHPAD,
262 translation_import_mode=
263 TranslationsBranchImportMode.IMPORT_TRANSLATIONS)
264 expected = [
265 {
266 'name': 'shared-template',
267 'status': 'shared',
268 'package_template': self.shared_template_ubuntu_side,
269 'upstream_template': self.shared_template_upstream_side,
270 },
271 {
272 'name': 'ubuntu-only',
273 'status': 'only in Ubuntu',
274 'package_template': self.ubuntu_only_template,
275 'upstream_template': None,
276 },
277 {
278 'name': 'upstream-only',
279 'status': 'only in upstream',
280 'package_template': None,
281 'upstream_template': self.upstream_only_template,
282 },
283 ]
284 self.assertEqual(expected, self.view.template_info())
285
286
287class TestSourcePackageSharingDetailsPage(BrowserTestCase,
288 ConfigureUpstreamProjectMixin):
289 """Test for the sharing details page of a source package."""
290
291 layer = DatabaseFunctionalLayer
292
293 def setUp(self):
294 super(TestSourcePackageSharingDetailsPage, self).setUp()
295 self.useFixture(FeatureFixture(
296 {'translations.sharing_information.enabled': 'on'}))
297
298 def _makeSourcePackage(self):
299 """Make a source package in Ubuntu."""
300 distroseries = self.factory.makeUbuntuDistroSeries()
301 return self.factory.makeSourcePackage(distroseries=distroseries)
302
303 def _makeFullyConfiguredSharing(self):
304 """Setup a fully configured sharing scenario."""
305 packaging = self.factory.makePackagingLink(in_ubuntu=True)
306 productseries = packaging.productseries
307 sourcepackage = packaging.sourcepackage
308 self.configureUpstreamProject(
309 productseries,
310 set_upstream_branch=True,
311 translations_usage=ServiceUsage.LAUNCHPAD,
312 translation_import_mode=(
313 TranslationsBranchImportMode.IMPORT_TRANSLATIONS))
314 return (sourcepackage, productseries)
315
316 def _getSharingDetailsViewBrowser(self, sourcepackage):
317 return self.getViewBrowser(
318 sourcepackage, no_login=True, rootsite="translations",
319 view_name="+sharing-details")
320
321 def test_checklist_unconfigured(self):
322 # Without a packaging link, sharing is completely unconfigured
323 sourcepackage = self._makeSourcePackage()
324 browser = self._getSharingDetailsViewBrowser(sourcepackage)
325 checklist = find_tag_by_id(browser.contents, 'sharing-checklist')
326 self.assertIsNot(None, checklist)
327 self.assertTextMatchesExpressionIgnoreWhitespace("""
328 Translation sharing configuration is incomplete.
329 No upstream project series has been linked. Change upstream link
330 No source branch exists for the upstream series.
331 Translations are not enabled on the upstream series.
332 Automatic synchronization of translations is not enabled.""",
333 extract_text(checklist))
334
335 def test_checklist_partly_configured(self):
336 # Linking a source package takes care of one item.
337 packaging = self.factory.makePackagingLink(in_ubuntu=True)
338 browser = self._getSharingDetailsViewBrowser(packaging.sourcepackage)
339 checklist = find_tag_by_id(browser.contents, 'sharing-checklist')
340 self.assertIsNot(None, checklist)
341 self.assertTextMatchesExpressionIgnoreWhitespace("""
342 Translation sharing configuration is incomplete.
343 Linked upstream series is .+ trunk series.
344 Change upstream link Remove upstream link
345 No source branch exists for the upstream series.
346 Translations are not enabled on the upstream series.
347 Automatic synchronization of translations is not enabled.""",
348 extract_text(checklist))
349
350 def test_checklist_fully_configured(self):
351 # A fully configured sharing setup.
352 sourcepackage = self._makeFullyConfiguredSharing()[0]
353 browser = self._getSharingDetailsViewBrowser(sourcepackage)
354 checklist = find_tag_by_id(browser.contents, 'sharing-checklist')
355 self.assertIsNot(None, checklist)
356 self.assertTextMatchesExpressionIgnoreWhitespace("""
357 Translation sharing with upstream is active.
358 Linked upstream series is .+ trunk series.
359 Change upstream link Remove upstream link
360 Upstream source branch is .+[.]
361 Translations are enabled on the upstream project.
362 Automatic synchronization of translations is enabled.""",
363 extract_text(checklist))
364
365 def test_potlist_only_ubuntu(self):
366 # Without a packaging link, only Ubuntu templates are listed.
367 sourcepackage = self._makeSourcePackage()
368 self.factory.makePOTemplate(
369 name='foo-template', sourcepackage=sourcepackage)
370 browser = self._getSharingDetailsViewBrowser(sourcepackage)
371 tbody = find_tag_by_id(
372 browser.contents, 'template-table').find('tbody')
373 self.assertIsNot(None, tbody)
374 self.assertTextMatchesExpressionIgnoreWhitespace("""
375 foo-template only in Ubuntu 0 \d+ second(s)? ago""",
376 extract_text(tbody))
377
378 def test_potlist_sharing(self):
379 # With sharing configured, templates on both sides are listed.
380 sourcepackage, productseries = self._makeFullyConfiguredSharing()
381 template_name = 'foo-template'
382 self.factory.makePOTemplate(
383 name=template_name, sourcepackage=sourcepackage)
384 self.factory.makePOTemplate(
385 name=template_name, productseries=productseries)
386 browser = self._getSharingDetailsViewBrowser(sourcepackage)
387 tbody = find_tag_by_id(
388 browser.contents, 'template-table').find('tbody')
389 self.assertIsNot(None, tbody)
390 self.assertTextMatchesExpressionIgnoreWhitespace("""
391 foo-template shared
392 0 \d+ second(s)? ago 0 \d+ second(s)? ago""",
393 extract_text(tbody))
394
395 def test_potlist_only_upstream(self):
396 # A template that is only present in upstream is called
397 # "only in upstream".
398 sourcepackage, productseries = self._makeFullyConfiguredSharing()
399 template_name = 'foo-template'
400 self.factory.makePOTemplate(
401 name=template_name, productseries=productseries)
402 browser = self._getSharingDetailsViewBrowser(sourcepackage)
403 tbody = find_tag_by_id(
404 browser.contents, 'template-table').find('tbody')
405 self.assertIsNot(None, tbody)
406 self.assertTextMatchesExpressionIgnoreWhitespace("""
407 foo-template only in upstream 0 \d+ second(s)? ago""",
408 extract_text(tbody))
409
410 def test_message_no_templates(self):
411 # When sharing is fully configured but no upstream templates are
412 # found, a message is displayed.
413 sourcepackage = self._makeFullyConfiguredSharing()[0]
414 browser = self._getSharingDetailsViewBrowser(sourcepackage)
415 self.assertEqual(
416 ["No upstream templates have been found yet. Please follow "
417 "the import process by going to the Translation Import Queue "
418 "of the upstream project series."],
419 get_feedback_messages(browser.contents))
420
421 def test_no_message_with_templates(self):
422 # When sharing is fully configured and templates are found, no
423 # message should be displayed.
424 sourcepackage, productseries = self._makeFullyConfiguredSharing()
425 self.factory.makePOTemplate(productseries=productseries)
426 browser = self._getSharingDetailsViewBrowser(sourcepackage)
427 self.assertEqual([], get_feedback_messages(browser.contents))
428
429 def test_no_message_with_incomplate_sharing(self):
430 # When sharing is not fully configured and templates are found, no
431 # message should be displayed.
432 packaging = self.factory.makePackagingLink(in_ubuntu=True)
433 productseries = packaging.productseries
434 sourcepackage = packaging.sourcepackage
435 self.factory.makePOTemplate(productseries=productseries)
436 browser = self._getSharingDetailsViewBrowser(sourcepackage)
437 self.assertEqual([], get_feedback_messages(browser.contents))
0438
=== added file 'lib/lp/translations/templates/sourcepackage-sharing-details.pt'
--- lib/lp/translations/templates/sourcepackage-sharing-details.pt 1970-01-01 00:00:00 +0000
+++ lib/lp/translations/templates/sourcepackage-sharing-details.pt 2011-03-15 19:18:37 +0000
@@ -0,0 +1,121 @@
1<html
2 xmlns="http://www.w3.org/1999/xhtml"
3 xmlns:tal="http://xml.zope.org/namespaces/tal"
4 xmlns:metal="http://xml.zope.org/namespaces/metal"
5 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
6 metal:use-macro="view/macro:page/main_only"
7 i18n:domain="launchpad"
8>
9 <body>
10 <div metal:fill-slot="heading">
11 <h1>Translation sharing details</h1>
12 </div>
13 <div metal:fill-slot="main">
14 <dl id="sharing-checklist">
15 <dt><span tal:condition="not:view/is_configuration_complete">
16 Translation sharing configuration is incomplete.
17 </span>
18 <span tal:condition="view/is_configuration_complete">
19 Translation sharing with upstream is active.
20 </span>
21 </dt>
22 <dd>
23 <ul>
24 <li class="sprite no"
25 tal:condition="not:view/is_packaging_configured">
26 No upstream project series has been linked.
27 <a tal:replace="structure context/menu:overview/edit_packaging/fmt:icon" />
28 </li>
29 <li class="sprite yes"
30 tal:condition="view/is_packaging_configured">
31 Linked upstream series is
32 <a tal:replace="structure context/productseries/fmt:link">
33 Gimp trunk</a>.
34 <a tal:replace="structure context/menu:overview/edit_packaging/fmt:icon" />
35 <a tal:replace="structure context/menu:overview/remove_packaging/fmt:icon" />
36 </li>
37 <li tal:attributes="class view/no_item_class"
38 tal:condition="not:view/has_upstream_branch">
39 No source branch exists for the upstream series.
40 <a tal:condition="view/is_packaging_configured"
41 tal:replace="structure context/productseries/menu:overview/set_branch/fmt:icon" />
42 </li>
43 <li class="sprite yes"
44 tal:condition="view/has_upstream_branch">
45 Upstream source branch is
46 <a tal:replace="structure context/productseries/branch/fmt:link">
47 lp:gimp</a>.
48 <a tal:condition="view/is_packaging_configured"
49 tal:replace="structure context/productseries/menu:overview/set_branch/fmt:icon" />
50 </li>
51 <li tal:attributes="class view/no_item_class"
52 tal:condition="not:view/is_upstream_translations_enabled">
53 Translations are not enabled on the upstream series.
54 <a tal:condition="view/is_packaging_configured"
55 tal:replace="structure context/productseries/product/menu:translations/settings/fmt:icon" />
56 </li>
57 <li class="sprite yes"
58 tal:condition="view/is_upstream_translations_enabled">
59 Translations are enabled on the upstream project.
60 <a tal:condition="view/is_packaging_configured"
61 tal:replace="structure context/productseries/product/menu:translations/settings/fmt:icon" />
62 </li>
63 <li tal:attributes="class view/no_item_class"
64 tal:condition="not:view/is_upstream_synchronization_enabled">
65 Automatic synchronization of translations is not enabled.
66 <a tal:condition="view/is_packaging_configured"
67 tal:replace="structure context/productseries/menu:translations/settings/fmt:icon" />
68 </li>
69 <li class="sprite yes"
70 tal:condition="view/is_upstream_synchronization_enabled">
71 Automatic synchronization of translations is enabled.
72 <a tal:condition="view/is_packaging_configured"
73 tal:replace="structure context/productseries/menu:translations/settings/fmt:icon" />
74 </li>
75 </ul>
76 </dd>
77 </dl>
78 <table class="listing" id="template-table">
79 <thead>
80 <tr>
81 <th class="name_column" rowspan="2">Template name</th>
82 <th class="state_column" rowspan="2">State</th>
83 <th style="text-align: center" colspan="2">Ubuntu</th>
84 <th style="text-align: center" colspan="3">Upstream</th>
85 </tr>
86 <tr>
87 <th class="length_ubuntu_column">Length</th>
88 <th class="updated_ubuntu_column">Updated</th>
89 <th class="length_upstream_column">Length</th>
90 <th class="updated_upstream_column">Updated</th>
91 <th class="upstream_link_column">Upstream template</th>
92 </tr>
93 </thead>
94 <tbody>
95 <tr tal:repeat="info view/template_info">
96 <td class="name_column">
97 <a tal:content="info/name"
98 tal:attributes="href info/package_template/fmt:url"
99 tal:omit-tag="not:info/package_template">gimp20</a></td>
100 <td class="state_column" tal:content="info/status">sharing</td>
101 <td class="length_ubuntu_column">
102 <tal:content tal:condition="info/package_template"
103 tal:content="info/package_template/messagecount">82</tal:content></td>
104 <td class="updated_ubuntu_column">
105 <tal:content tal:condition="info/package_template"
106 tal:content="info/package_template/date_last_updated/fmt:approximatedate">2 minutes ago</tal:content></td>
107 <td class="length_upstream_column">
108 <tal:content tal:condition="info/upstream_template"
109 tal:content="info/upstream_template/messagecount">85</tal:content></td>
110 <td class="updated_upstream_column">
111 <tal:content tal:condition="info/upstream_template"
112 tal:content="info/upstream_template/date_last_updated/fmt:approximatedate">2 minutes ago</tal:content></td>
113 <td class="upstream_link_column">
114 <a tal:condition="info/upstream_template"
115 tal:attributes="href info/upstream_template/fmt:url">View upstream</a></td>
116 </tr>
117 </tbody>
118 </table>
119 </div>
120 </body>
121</html>