Merge lp:~henninge/launchpad/bug-488765-oops-translations into lp:launchpad

Proposed by Henning Eggers
Status: Merged
Approved by: Henning Eggers
Approved revision: not available
Merged at revision: not available
Proposed branch: lp:~henninge/launchpad/bug-488765-oops-translations
Merge into: lp:launchpad
Diff against target: 236 lines (+88/-51)
4 files modified
lib/lp/translations/browser/product.py (+22/-25)
lib/lp/translations/browser/tests/test_product_view.py (+2/-1)
lib/lp/translations/stories/standalone/xx-product-translations.txt (+54/-15)
lib/lp/translations/templates/product-translations.pt (+10/-10)
To merge this branch: bzr merge lp:~henninge/launchpad/bug-488765-oops-translations
Reviewer Review Type Date Requested Status
Guilherme Salgado (community) code Approve
Canonical Launchpad Engineering code Pending
Review via email: mp+17396@code.launchpad.net

Commit message

Fixed Product:+translations view to always display something and not to oops on source packages.

To post a comment you must log in.
Revision history for this message
Henning Eggers (henninge) wrote :

= Bug 488765 =

A product has an attribute primary_translatable that returns the product series or source package that should be translated. Returning source packages here seems to be a newer feature that broke a product's +translations page if the primary_translatable was a source package because the template assumed it's a product series.

== Proposed fx ==

I filed bug 507534 about that behaviour and fixed the page by filtering out source packages in the view so that only product series are displayed by the template.

I also discovered that bug 371632 can be fixed with this.

== Implementation details ==

lib/lp/translations/browser/product.py

 * The view already had a property "primary_translatable" that was obviously unused and returned a dictionary. I re-used it to return the primary_translatable of the context or None if it is not a product series.

lib/lp/translations/stories/standalone/xx-product-translations.txt

 * The part that only checked for up/download links was expanded to check for the whole recommendation section. This way it can be tested for complete absence if no primary_translatable is available.

 * This test did reproducce the error because evolution is linked to a translatable source package in the sample data, so Product.primary_translatable returns a source package when all templates in the product series have been disabled.

 * Disabling all the templates is a bit of noise but needed to reproduce the error.

 * Appended a test to show the notice when no translations are available.

lib/lp/translations/templates/product-translations.pt

 * Use view/primary_translatable instead of context's.

 * Changed some ids and classes needed for testing.

 * Added notice for when no translations are available.

 * Changed conditions so that page is never empty.

== Test ==

bin/test -vvct product-translations

== Demo/QA ==

On launchpad.dev:
 1. Disable all templates for evolution trunk.
 2. Got to https://translations.launchpad.dev/evolution
 3. You should not get an oops but a notice that there are no translations for this project.

To reproduce on staging you'll need a project that is linked to a source pacakage with translations.

= 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/product.py
  lib/lp/translations/stories/standalone/xx-product-translations.txt
  lib/lp/translations/templates/product-translations.pt

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

Hi Henning,

I have just a few suggestions to improve this branch a bit.

 review needs-fixing

On Thu, 2010-01-14 at 17:22 +0000, Henning Eggers wrote:
> = Bug 488765 =
>
> A product has an attribute primary_translatable that returns the
> product series or source package that should be translated. Returning
> source packages here seems to be a newer feature that broke a
> product's +translations page if the primary_translatable was a source
> package because the template assumed it's a product series.
>
> == Proposed fx ==
>
> I filed bug 507534 about that behaviour and fixed the page by
> filtering out source packages in the view so that only product series
> are displayed by the template.
>
> I also discovered that bug 371632 can be fixed with this.
>
> == Implementation details ==
>
> lib/lp/translations/browser/product.py
>
> * The view already had a property "primary_translatable" that was
> obviously unused and returned a dictionary. I re-used it to return the
> primary_translatable of the context or None if it is not a product
> series.
>
> lib/lp/translations/stories/standalone/xx-product-translations.txt
>
> * The part that only checked for up/download links was expanded to
> check for the whole recommendation section. This way it can be tested
> for complete absence if no primary_translatable is available.
>
> * This test did reproducce the error because evolution is linked to a
> translatable source package in the sample data, so
> Product.primary_translatable returns a source package when all
> templates in the product series have been disabled.
>
> * Disabling all the templates is a bit of noise but needed to
> reproduce the error.
>
> * Appended a test to show the notice when no translations are
> available.
>
> lib/lp/translations/templates/product-translations.pt
>
> * Use view/primary_translatable instead of context's.
>
> * Changed some ids and classes needed for testing.
>
> * Added notice for when no translations are available.
>
> * Changed conditions so that page is never empty.
>
>
> == Test ==
>
> bin/test -vvct product-translations
>
> == Demo/QA ==
>
> On launchpad.dev:
> 1. Disable all templates for evolution trunk.
> 2. Got to https://translations.launchpad.dev/evolution
> 3. You should not get an oops but a notice that there are no
> translations for this project.
>
> To reproduce on staging you'll need a project that is linked to a
> source pacakage with translations.
>

> === modified file 'lib/lp/translations/browser/product.py'
> --- lib/lp/translations/browser/product.py 2009-10-26 18:40:04 +0000
> +++ lib/lp/translations/browser/product.py 2010-01-15 07:41:14 +0000
> @@ -89,28 +89,14 @@
>
> @cachedproperty
> def primary_translatable(self):
> - """Return a dictionary with the info for a primary translatable.
> -
> - If there is no primary translatable object, returns an empty
> - dictionary.
> -
> - The dictionary has the keys:
> - * 'title': The title of the translatable object.
> - * 'potemplates': a set of PO Templates for this object.
> - * 'base_url': The base URL to reach the base URL for this object.
...

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

Am 15.01.2010 13:06, Guilherme Salgado schrieb:
> Review: Needs Fixing
> Hi Henning,
>
> I have just a few suggestions to improve this branch a bit.

Cool, thanks for doing the review.

>> === modified file 'lib/lp/translations/browser/product.py'
>> + if not isinstance(removeSecurityProxy(translatable), ProductSeries):
>
> You can use zope.security.proxy.isinstance here to avoid having to
> remove the security proxy.

Cool, I didn't know about this.

>
> Actually, you should use IProductSeries.providedBy(translatable), which
> would allow you to not import ProductSeries here, as that must not be
> done. I wonder why the import fascist doesn't emit a warning about this
> import...

Yes, you are right on both accounts. I changed it to use providedBy.

>> === modified file 'lib/lp/translations/stories/standalone/xx-product-translations.txt'
>> +A series is not translatable if all templates are disabled. We need to jump
>> +through some hoops to create that situation.
>> +
>> + >>> login('<email address hidden>')
>> + >>> from zope.component import getUtility
>> + >>> from lp.registry.interfaces.product import IProductSet
>> + >>> evotrunk = getUtility(IProductSet).getByName(
>> + ... 'evolution').getSeries('trunk')
>> + >>> from lp.translations.interfaces.potemplate import IPOTemplateSet
>> + >>> potemplates = getUtility(IPOTemplateSet).getSubset(
>> + ... productseries=evotrunk, iscurrent=True)
>> + >>> for potemplate in potemplates:
>> + ... potemplate.iscurrent = False
>> + >>> logout()
>> + >>> admin_browser.open(product_url)
>> + >>> print find_translation_recommendation(admin_browser)
>> + None
>
> I think it'd be nice to show here that the product has a translatable
> source package and explain that is only in that case that we don't show
> recommendations. It's also important to do that because the test
> assumes there's a translatable source package associated to evolution
> (as you described in the cover letter), but the test itself doesn't make
> it clear nor does it assert that.

Ah, that is not quite right. We don't show the recommendation even if
there is source package because atm we cannot treat a source package
like a product series in this respect. That will have to be fixed in
another branch (bug 507534).

I added a test that shows that the series has translatable source
package. This situation is what triggered the oops that this branch is
fixing but the display of recommendations is independent of a
translatable source package.

>
>> +
>> +Instead a notice is displayed that the product has no translations.
>> +
>> + >>> notice = first_tag_by_class(admin_browser.contents, 'notice')
>> + >>> print extract_text(notice)
>> + There are no translations for this project.
>>
>
>> === modified file 'lib/lp/translations/templates/product-translations.pt'
>> --- lib/lp/translations/templates/product-translations.pt 2009-12-16 15:21:36 +0000
>> +++ lib/lp/translations/templates/product-translations.pt 2010-01-15 07:41:14 +0000
>> @@ -14,7 +14,7 @@
>> </div>
>>
>> <div metal:fill-slot="main"
>> - tal:define="uses_translations view/uses_tr...

Read more...

=== modified file 'lib/lp/translations/browser/product.py'
--- lib/lp/translations/browser/product.py 2010-01-15 07:39:43 +0000
+++ lib/lp/translations/browser/product.py 2010-01-15 14:37:06 +0000
@@ -11,14 +11,12 @@
11 'ProductView',11 'ProductView',
12 ]12 ]
1313
14from zope.security.proxy import removeSecurityProxy
15
16from canonical.cachedproperty import cachedproperty14from canonical.cachedproperty import cachedproperty
17from canonical.launchpad.webapp import (15from canonical.launchpad.webapp import (
18 LaunchpadView, Link, canonical_url, enabled_with_permission)16 LaunchpadView, Link, canonical_url, enabled_with_permission)
17from canonical.launchpad.webapp.authorization import check_permission
19from canonical.launchpad.webapp.menu import NavigationMenu18from canonical.launchpad.webapp.menu import NavigationMenu
20from lp.registry.interfaces.product import IProduct19from lp.registry.interfaces.product import IProduct, IProductSeries
21from lp.registry.model.productseries import ProductSeries
22from lp.registry.browser.product import ProductEditView20from lp.registry.browser.product import ProductEditView
23from lp.translations.browser.translations import TranslationsMixin21from lp.translations.browser.translations import TranslationsMixin
2422
@@ -85,7 +83,20 @@
85 @cachedproperty83 @cachedproperty
86 def uses_translations(self):84 def uses_translations(self):
87 """Whether this product has translatable templates."""85 """Whether this product has translatable templates."""
88 return (self.context.official_rosetta and self.primary_translatable)86 return (self.context.official_rosetta and
87 self.primary_translatable is not None)
88
89 @cachedproperty
90 def no_translations_available(self):
91 """Has no translation templates but does support translations."""
92 return (self.context.official_rosetta and
93 self.primary_translatable is None)
94
95 @cachedproperty
96 def show_page_content(self):
97 """Whether the main content of the page should be shown."""
98 return (self.context.official_rosetta or
99 check_permission("launchpad.TranslationsAdmin", self.context))
89100
90 @cachedproperty101 @cachedproperty
91 def primary_translatable(self):102 def primary_translatable(self):
@@ -93,7 +104,7 @@
93 """104 """
94 translatable = self.context.primary_translatable105 translatable = self.context.primary_translatable
95106
96 if not isinstance(removeSecurityProxy(translatable), ProductSeries):107 if not IProductSeries.providedBy(translatable):
97 return None108 return None
98109
99 return translatable110 return translatable
100111
=== modified file 'lib/lp/translations/stories/standalone/xx-product-translations.txt'
--- lib/lp/translations/stories/standalone/xx-product-translations.txt 2010-01-14 17:03:56 +0000
+++ lib/lp/translations/stories/standalone/xx-product-translations.txt 2010-01-15 13:56:37 +0000
@@ -222,6 +222,15 @@
222 >>> print find_translation_recommendation(admin_browser)222 >>> print find_translation_recommendation(admin_browser)
223 None223 None
224224
225At the moment, translatable source packages are not recommended, although
226the product is linked to one.
227
228 >>> source_package = find_tag_by_id(
229 ... admin_browser.contents, 'portlet-translatable-packages')
230 >>> print extract_text(source_package)
231 All translatable distribution packages
232 “evolution” source package in Hoary
233
225Instead a notice is displayed that the product has no translations.234Instead a notice is displayed that the product has no translations.
226235
227 >>> notice = first_tag_by_class(admin_browser.contents, 'notice')236 >>> notice = first_tag_by_class(admin_browser.contents, 'notice')
228237
=== modified file 'lib/lp/translations/templates/product-translations.pt'
--- lib/lp/translations/templates/product-translations.pt 2010-01-15 07:39:43 +0000
+++ lib/lp/translations/templates/product-translations.pt 2010-01-15 14:39:59 +0000
@@ -14,8 +14,7 @@
14 </div>14 </div>
1515
16 <div metal:fill-slot="main"16 <div metal:fill-slot="main"
17 tal:define="official_rosetta context/official_rosetta;17 tal:define="admin_user context/required:launchpad.TranslationsAdmin">
18 admin_user context/required:launchpad.TranslationsAdmin">
19 <div class="translation-help-links">18 <div class="translation-help-links">
20 <a href="https://help.launchpad.net/Translations"19 <a href="https://help.launchpad.net/Translations"
21 id="link-to-translations-help"20 id="link-to-translations-help"
@@ -27,13 +26,12 @@
27 <tal:official_use replace="26 <tal:official_use replace="
28 structure27 structure
29 context/@@+portlet-not-using-launchpad"/>28 context/@@+portlet-not-using-launchpad"/>
30 <div tal:condition="python:not view.uses_translations and official_rosetta">29 <div tal:condition="view/no_translations_available">
31 There are no translations for this project.30 There are no translations for this project.
32 </div>31 </div>
33 </div>32 </div>
3433
35 <tal:uses-translations34 <tal:page_content condition="view/show_page_content">
36 condition="python:official_rosetta or admin_user">
37 <tal:translatable define="target view/primary_translatable">35 <tal:translatable define="target view/primary_translatable">
3836
39 <div class="yui-g">37 <div class="yui-g">
@@ -100,7 +98,7 @@
100 <div tal:replace="structure context/@@+rosetta-status-legend" />98 <div tal:replace="structure context/@@+rosetta-status-legend" />
101 </div>99 </div>
102 </tal:translatable>100 </tal:translatable>
103 </tal:uses-translations>101 </tal:page_content>
104 </div>102 </div>
105 </body>103 </body>
106</html>104</html>
Revision history for this message
Guilherme Salgado (salgado) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/translations/browser/product.py'
--- lib/lp/translations/browser/product.py 2009-10-26 18:40:04 +0000
+++ lib/lp/translations/browser/product.py 2010-01-16 08:51:16 +0000
@@ -11,14 +11,12 @@
11 'ProductView',11 'ProductView',
12 ]12 ]
1313
14from zope.security.proxy import removeSecurityProxy
15
16from canonical.cachedproperty import cachedproperty14from canonical.cachedproperty import cachedproperty
17from canonical.launchpad.webapp import (15from canonical.launchpad.webapp import (
18 LaunchpadView, Link, canonical_url, enabled_with_permission)16 LaunchpadView, Link, canonical_url, enabled_with_permission)
17from canonical.launchpad.webapp.authorization import check_permission
19from canonical.launchpad.webapp.menu import NavigationMenu18from canonical.launchpad.webapp.menu import NavigationMenu
20from lp.registry.interfaces.product import IProduct19from lp.registry.interfaces.product import IProduct, IProductSeries
21from lp.registry.model.productseries import ProductSeries
22from lp.registry.browser.product import ProductEditView20from lp.registry.browser.product import ProductEditView
23from lp.translations.browser.translations import TranslationsMixin21from lp.translations.browser.translations import TranslationsMixin
2422
@@ -85,32 +83,31 @@
85 @cachedproperty83 @cachedproperty
86 def uses_translations(self):84 def uses_translations(self):
87 """Whether this product has translatable templates."""85 """Whether this product has translatable templates."""
88 return (self.context.official_rosetta and self.primary_translatable)86 return (self.context.official_rosetta and
87 self.primary_translatable is not None)
88
89 @cachedproperty
90 def no_translations_available(self):
91 """Has no translation templates but does support translations."""
92 return (self.context.official_rosetta and
93 self.primary_translatable is None)
94
95 @cachedproperty
96 def show_page_content(self):
97 """Whether the main content of the page should be shown."""
98 return (self.context.official_rosetta or
99 check_permission("launchpad.TranslationsAdmin", self.context))
89100
90 @cachedproperty101 @cachedproperty
91 def primary_translatable(self):102 def primary_translatable(self):
92 """Return a dictionary with the info for a primary translatable.103 """Return the context's primary translatable if it's a product series.
93
94 If there is no primary translatable object, returns an empty
95 dictionary.
96
97 The dictionary has the keys:
98 * 'title': The title of the translatable object.
99 * 'potemplates': a set of PO Templates for this object.
100 * 'base_url': The base URL to reach the base URL for this object.
101 """104 """
102 translatable = self.context.primary_translatable105 translatable = self.context.primary_translatable
103 naked_translatable = removeSecurityProxy(translatable)106
104107 if not IProductSeries.providedBy(translatable):
105 if (translatable is None or108 return None
106 not isinstance(naked_translatable, ProductSeries)):109
107 return {}110 return translatable
108
109 return {
110 'title': translatable.title,
111 'potemplates': translatable.getCurrentTranslationTemplates(),
112 'base_url': canonical_url(translatable)
113 }
114111
115 @cachedproperty112 @cachedproperty
116 def untranslatable_series(self):113 def untranslatable_series(self):
117114
=== modified file 'lib/lp/translations/browser/tests/test_product_view.py'
--- lib/lp/translations/browser/tests/test_product_view.py 2009-07-17 02:25:09 +0000
+++ lib/lp/translations/browser/tests/test_product_view.py 2010-01-16 08:51:16 +0000
@@ -36,7 +36,8 @@
36 pot = self.factory.makePOTemplate(36 pot = self.factory.makePOTemplate(
37 distroseries=sourcepackage.distroseries,37 distroseries=sourcepackage.distroseries,
38 sourcepackagename=sourcepackage.sourcepackagename)38 sourcepackagename=sourcepackage.sourcepackagename)
39 self.assertEquals(self.view.primary_translatable, {})39 self.assertEquals(None, self.view.primary_translatable)
4040
41def test_suite():41def test_suite():
42 return unittest.TestLoader().loadTestsFromName(__name__)42 return unittest.TestLoader().loadTestsFromName(__name__)
43
4344
=== modified file 'lib/lp/translations/stories/standalone/xx-product-translations.txt'
--- lib/lp/translations/stories/standalone/xx-product-translations.txt 2009-11-01 20:18:32 +0000
+++ lib/lp/translations/stories/standalone/xx-product-translations.txt 2010-01-16 08:51:16 +0000
@@ -168,32 +168,71 @@
168 Language Untranslated Unreviewed Changed168 Language Untranslated Unreviewed Changed
169169
170170
171== Download and upload links ==171== Translation recommendation ==
172172
173A logged-in user is invited to download translations.173The page mentions which product series should be translated.
174174
175 >>> def find_download_upload_invitation(browser):175 >>> def find_translation_recommendation(browser):
176 ... """Find the text inviting the user to upload or download."""176 ... """Find the text recommending to translate."""
177 ... tag = find_tag_by_id(browser.contents, 'downloadupload')177 ... tag = find_tag_by_id(
178 ... browser.contents, 'translation-recommendation')
178 ... if tag is None:179 ... if tag is None:
179 ... return None180 ... return None
180 ... return extract_text(tag.renderContents())181 ... return extract_text(tag.renderContents())
181182
182 >>> product_url = 'http://translations.launchpad.dev/evolution'183 >>> product_url = 'http://translations.launchpad.dev/evolution'
183184
185That's all an anonymous user will see.
186
187 >>> anon_browser.open(product_url)
188 >>> print find_translation_recommendation(anon_browser)
189 Launchpad currently recommends translating Evolution trunk series.
190
191A logged-in user is also invited to download translations.
192
184 >>> user_browser.open(product_url)193 >>> user_browser.open(product_url)
185 >>> print find_download_upload_invitation(user_browser)194 >>> print find_translation_recommendation(user_browser)
195 Launchpad currently recommends translating Evolution trunk series.
186 You can also download translations for trunk.196 You can also download translations for trunk.
187197
188An anonymous user does not see this invitation.
189
190 >>> anon_browser.open(product_url)
191 >>> print find_download_upload_invitation(anon_browser)
192 None
193
194A user with upload rights sees the invitation not just to download but198A user with upload rights sees the invitation not just to download but
195to upload as well.199to upload as well.
196200
197 >>> admin_browser.open(product_url)201 >>> admin_browser.open(product_url)
198 >>> print find_download_upload_invitation(admin_browser)202 >>> print find_translation_recommendation(admin_browser)
203 Launchpad currently recommends translating Evolution trunk series.
199 You can also download or upload translations for trunk.204 You can also download or upload translations for trunk.
205
206If there is no translatable series, no such recommendation is displayed.
207A series is not translatable if all templates are disabled. We need to jump
208through some hoops to create that situation.
209
210 >>> login('foo.bar@canonical.com')
211 >>> from zope.component import getUtility
212 >>> from lp.registry.interfaces.product import IProductSet
213 >>> evotrunk = getUtility(IProductSet).getByName(
214 ... 'evolution').getSeries('trunk')
215 >>> from lp.translations.interfaces.potemplate import IPOTemplateSet
216 >>> potemplates = getUtility(IPOTemplateSet).getSubset(
217 ... productseries=evotrunk, iscurrent=True)
218 >>> for potemplate in potemplates:
219 ... potemplate.iscurrent = False
220 >>> logout()
221 >>> admin_browser.open(product_url)
222 >>> print find_translation_recommendation(admin_browser)
223 None
224
225At the moment, translatable source packages are not recommended, although
226the product is linked to one.
227
228 >>> source_package = find_tag_by_id(
229 ... admin_browser.contents, 'portlet-translatable-packages')
230 >>> print extract_text(source_package)
231 All translatable distribution packages
232 “evolution” source package in Hoary
233
234Instead a notice is displayed that the product has no translations.
235
236 >>> notice = first_tag_by_class(admin_browser.contents, 'notice')
237 >>> print extract_text(notice)
238 There are no translations for this project.
200239
=== modified file 'lib/lp/translations/templates/product-translations.pt'
--- lib/lp/translations/templates/product-translations.pt 2009-12-16 15:21:36 +0000
+++ lib/lp/translations/templates/product-translations.pt 2010-01-16 08:51:16 +0000
@@ -14,8 +14,7 @@
14 </div>14 </div>
1515
16 <div metal:fill-slot="main"16 <div metal:fill-slot="main"
17 tal:define="uses_translations view/uses_translations;17 tal:define="admin_user context/required:launchpad.TranslationsAdmin">
18 admin_user context/required:launchpad.TranslationsAdmin">
19 <div class="translation-help-links">18 <div class="translation-help-links">
20 <a href="https://help.launchpad.net/Translations"19 <a href="https://help.launchpad.net/Translations"
21 id="link-to-translations-help"20 id="link-to-translations-help"
@@ -23,26 +22,27 @@
23 </a>22 </a>
24 <div></div><!-- to clear-up all floats -->23 <div></div><!-- to clear-up all floats -->
25 </div>24 </div>
26 <div class="top-portlet">25 <div class="top-portlet notice">
27 <tal:official_use replace="26 <tal:official_use replace="
28 structure27 structure
29 context/@@+portlet-not-using-launchpad"/>28 context/@@+portlet-not-using-launchpad"/>
29 <div tal:condition="view/no_translations_available">
30 There are no translations for this project.
31 </div>
30 </div>32 </div>
3133
32 <tal:uses-translations34 <tal:page_content condition="view/show_page_content">
33 condition="python:uses_translations or admin_user">35 <tal:translatable define="target view/primary_translatable">
34 <tal:translatable define="target context/primary_translatable">
3536
36 <div class="yui-g">37 <div class="yui-g">
37 <div class="yui-u first">38 <div class="yui-u first">
38 <div class="portlet">39 <div class="portlet">
39 <h3>Translation details</h3>40 <h3>Translation details</h3>
40 <p tal:condition="target">41 <p tal:condition="target" id="translation-recommendation">
41 Launchpad currently recommends translating42 Launchpad currently recommends translating
42 <tal:target replace="structure target/fmt:link"43 <tal:target replace="structure target/fmt:link"
43 >trunk</tal:target>.44 >trunk</tal:target>.
44 <span id="downloadupload"45 <span tal:condition="context/required:launchpad.AnyPerson">
45 tal:condition="context/required:launchpad.AnyPerson">
46 You can also46 You can also
47 <a tal:attributes="href target/fmt:url/+export"47 <a tal:attributes="href target/fmt:url/+export"
48 >download</a>48 >download</a>
@@ -98,7 +98,7 @@
98 <div tal:replace="structure context/@@+rosetta-status-legend" />98 <div tal:replace="structure context/@@+rosetta-status-legend" />
99 </div>99 </div>
100 </tal:translatable>100 </tal:translatable>
101 </tal:uses-translations>101 </tal:page_content>
102 </div>102 </div>
103 </body>103 </body>
104</html>104</html>