Merge lp:~jtv/launchpad/translationimportqueueentry-info into lp:launchpad

Proposed by Jeroen T. Vermeulen
Status: Merged
Approved by: Jeroen T. Vermeulen
Approved revision: not available
Merged at revision: not available
Proposed branch: lp:~jtv/launchpad/translationimportqueueentry-info
Merge into: lp:launchpad
Diff against target: 586 lines (+459/-40)
7 files modified
lib/lp/translations/browser/configure.zcml (+1/-0)
lib/lp/translations/browser/tests/test_translationimportqueueentry.py (+164/-0)
lib/lp/translations/browser/translationimportqueue.py (+86/-4)
lib/lp/translations/stories/importqueue/xx-entry-details.txt (+106/-0)
lib/lp/translations/stories/importqueue/xx-entry-error-output.txt (+46/-0)
lib/lp/translations/templates/translationimportqueueentry-index.pt (+13/-0)
lib/lp/translations/templates/translationimportqueueentry-portlet-details.pt (+43/-36)
To merge this branch: bzr merge lp:~jtv/launchpad/translationimportqueueentry-info
Reviewer Review Type Date Requested Status
Curtis Hovey (community) ui Approve
Henning Eggers (community) code Approve
Review via email: mp+18963@code.launchpad.net

Commit message

Show translation upload details on approval form.

To post a comment you must log in.
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

= Bug 519556 =

When reviewing translations uploads, we translations admins often want quick access to the following information on the approval page:
 * The uploader.
 * The file's contents.
 * The project: what's its license status, is it already using Translations?
 * The release series: does it have any templates yet, and which ones?
 * When was the entry last updated? The current UI fails to show this anywhere.

In addition, it may occasionally be convenient to have a copy of the entry's error_output shown. That makes this page a great place to go when analyzing failures.

In this branch I add all of this information. It's at the bottom of the page, since I want it to be unobtrusive. It wouldn't do to force admins to scroll down before they get to the input fields. (Only translations admins and the Ubuntu translation coordinates can access this page).

For Q/A, I intend to visit a whole bunch of pages for translation import queue entries and see that they all make sense.

To test:
{{{
./bin/test -vv -t importqueue/xx-entry
}}}

No lint.

Jeroen

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

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

Hi Jeroen,

thanks for working late and creating this branch. Great work!

Am 10.02.2010 00:36, Jeroen T. Vermeulen schrieb:
> = Bug 519556 =
>
> When reviewing translations uploads, we translations admins often want quick access to the following information on the approval page:
> * The uploader.
> * The file's contents.
> * The project: what's its license status, is it already using Translations?
> * The release series: does it have any templates yet, and which ones?
> * When was the entry last updated? The current UI fails to show this anywhere.

Wonderful! That is very helpful information!

>
> In addition, it may occasionally be convenient to have a copy of the entry's error_output shown. That makes this page a great place to go when analyzing failures.

Actually, as you know, I have a branch ready that displays the
error_output in a text field as part of the form, so this part might go
out again, soon. No offence meant. ;-)

>
> In this branch I add all of this information. It's at the bottom of the page, since I want it to be unobtrusive. It wouldn't do to force admins to scroll down before they get to the input fields. (Only translations admins and the Ubuntu translation coordinates can access this page).

Yes, I agree with that, although a two-column layout wouldn't have been
bad, either. The form would go on the left, the information on the
right. But I am not sure if that is easy to do and would fit with our UI
guidelines.

As I can see, you managed to pull this off without adding any view code.
Impressive. ;-) I am going to ask you, though, to move some things to
the view code to keep the TAL clearer. Hope you can go along with that.

 review needs-fixing code

Cheers,
Henning

> === added file 'lib/lp/translations/stories/importqueue/xx-entry-details.txt'
> --- lib/lp/translations/stories/importqueue/xx-entry-details.txt 1970-01-01 00:00:00 +0000
> +++ lib/lp/translations/stories/importqueue/xx-entry-details.txt 2010-02-09 23:36:23 +0000
> @@ -0,0 +1,121 @@
> += Entry details =
> +
> +The translation import queue entry page shows various details about an
> +entry and its target that may be helpful in queue review.
> +
> + >>> from zope.security.proxy import removeSecurityProxy
> + >>> from lp.translations.model.translationimportqueue import (
> + ... TranslationImportQueue)
> +
> + >>> filename = 'po/foo.pot'
> +
> + >>> login(ANONYMOUS)
> + >>> queue = TranslationImportQueue()
> + >>> product = factory.makeProduct()
> + >>> removeSecurityProxy(product).official_rosetta = True

I wonder if removeSecurityProxy would be necessary here if you logged in
as foo.bar?

> + >>> trunk = product.getSeries('trunk')
> + >>> uploader = factory.makePerson()
> + >>> entry = queue.addOrUpdateEntry(
> + ... filename, '# empty', False, uploader, productseries=trunk)
> + >>> entry_url = canonical_url(entry, rootsite='translations')
> + >>> logout()
> +
> + >>> admin_browser.open(entry_url)
> + >>> details = find_tag_by_id(admin_browser.contents, 'portlet-details')
> + >>> details_text = extract_text(details.renderContents())
> + ...

review: Needs Fixing (code)
Revision history for this message
Henning Eggers (henninge) wrote :

For the UI review, here are two screenshots, one of an entry for a template file with error outpout, one for a normal translation file without error output. The review is just about the stuff that comes under the actions.

http://people.canonical.com/~henninge/screenshots/import-queue-entry-info-pot.png
http://people.canonical.com/~henninge/screenshots/import-queue-entry-info-po.png

Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :
Download full text (7.9 KiB)

Hi Henning, thank you for the surprise review! And a thorough job you did of it, too. Glad you like it, because this branch did contribute to a headache. :-)

> Actually, as you know, I have a branch ready that displays the
> error_output in a text field as part of the form, so this part might go
> out again, soon. No offence meant. ;-)

I considered that. But the error_output can be pages and pages of text in some cases. Viewing those in the queue UI isn't very convenient, and neither is scrolling through long and wide text in an input box. So I figured we might sometimes appreciate having it displayed more plainly. It's an extra, so I did put it at the bottom of the page.

> Yes, I agree with that, although a two-column layout wouldn't have been
> bad, either. The form would go on the left, the information on the
> right. But I am not sure if that is easy to do and would fit with our UI
> guidelines.

I'm not sure we can do that with generic forms. I did try it with the yui table layout, but had no luck.

In any case, this is nice and compact, no?

> As I can see, you managed to pull this off without adding any view code.
> Impressive. ;-) I am going to ask you, though, to move some things to
> the view code to keep the TAL clearer. Hope you can go along with that.

Quite right you are. TAL can be nice for prototyping though.

> > === added file 'lib/lp/translations/stories/importqueue/xx-entry-
> details.txt'
> > --- lib/lp/translations/stories/importqueue/xx-entry-details.txt
> 1970-01-01 00:00:00 +0000
> > +++ lib/lp/translations/stories/importqueue/xx-entry-details.txt
> 2010-02-09 23:36:23 +0000
> > @@ -0,0 +1,121 @@
> > += Entry details =
> > +
> > +The translation import queue entry page shows various details about an
> > +entry and its target that may be helpful in queue review.
> > +
> > + >>> from zope.security.proxy import removeSecurityProxy
> > + >>> from lp.translations.model.translationimportqueue import (
> > + ... TranslationImportQueue)
> > +
> > + >>> filename = 'po/foo.pot'
> > +
> > + >>> login(ANONYMOUS)
> > + >>> queue = TranslationImportQueue()
> > + >>> product = factory.makeProduct()
> > + >>> removeSecurityProxy(product).official_rosetta = True
>
> I wonder if removeSecurityProxy would be necessary here if you logged in
> as foo.bar?

Maybe not, but it's yet another dependency on the test data with implicit meaning ("logging in as this user because it's an admin"). Stripping the proxy was easy enough.

> > +In that case, the product is also shown to have translatable series.
> > +
> > + >>> print details_text
> > + Upload attached to
> > + ...
> > + Project has translatable series.
> > + ...
> > +
>
> Would it be too much to list these series here?

Almost, but I did it anyway. :-) It now lists up to three series (with linkified names), and if there's more, it adds an ellipsis.

> > +The string "1 template" neatly adjusts to the actual number of
> > +templates.
> > +
> > + >>> login(ANONYMOUS)
> > + >>> template = factory.makePOTemplate(productseries=trunk)
> > + >>> logout()
> > +
> > + >>> admin_browser.open(entry_url)
> > + >>> detail...

Read more...

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

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

Hi Jeroen,
thanks for this improvement. I have a few little issues but please land
this when you are done with them.

 review approve code

Cheers,
Henning

> === added file 'lib/lp/translations/browser/tests/test_translationimportqueueentry.py'
> --- lib/lp/translations/browser/tests/test_translationimportqueueentry.py 1970-01-01 00:00:00 +0000
> +++ lib/lp/translations/browser/tests/test_translationimportqueueentry.py 2010-02-10 13:06:21 +0000
> @@ -0,0 +1,165 @@
> +# Copyright 2010 Canonical Ltd. This software is licensed under the
> +# GNU Affero General Public License version 3 (see the file LICENSE).
> +
> +"""Unit tests for translation import queue views."""
> +
> +from datetime import datetime
> +from pytz import timezone
> +from unittest import TestLoader
> +
> +from zope.component import getMultiAdapter
> +from zope.security.proxy import removeSecurityProxy
> +
> +from canonical.testing import LaunchpadFunctionalLayer
> +
> +from canonical.launchpad.layers import TranslationsLayer, setFirstLayer
> +from canonical.launchpad.webapp import canonical_url
> +from canonical.launchpad.webapp.servers import LaunchpadTestRequest
> +from lp.registry.model.sourcepackage import SourcePackage
> +from lp.testing import TestCaseWithFactory
> +from lp.translations.model.translationimportqueue import (
> + TranslationImportQueue)

Don't you get an warnings about this? Should test code be importing from
 model code? I thought I remember that rule (and it makes sense if we
profess to test against Interfaces).

> +
> +
> +class TestTranslationImportQueueEntryView(TestCaseWithFactory):
> + """Tests for the queue entry review form."""
> +
> + layer = LaunchpadFunctionalLayer
> +
> + def setUp(self):
> + super(TestTranslationImportQueueEntryView, self).setUp(
> + '<email address hidden>')
> + self.queue = TranslationImportQueue()

I think this should really be:
  self.queue = getUtility(ITranslationImportQueue)

> + self.uploader = self.factory.makePerson()
> +
> + def _makeProductSeries(self):
> + """Set up a product series for a translatable product."""
> + product = self.factory.makeProduct()
> + product.official_rosetta = True
> + return product.getSeries('trunk')
> +
> + def _makeView(self, entry):
> + """Create view for a queue entry."""
> + request = LaunchpadTestRequest()
> + setFirstLayer(request, TranslationsLayer)
> + view = getMultiAdapter((entry, request), name='+index')
> + view.initialize()
> + return view
> +
> + def _makeEntry(self, productseries=None, distroseries=None,
> + sourcepackagename=None):
> + filename = self.factory.getUniqueString() + '.pot'
> + contents = self.factory.getUniqueString()
> + entry = self.queue.addOrUpdateEntry(
> + filename, contents, False, self.uploader,
> + productseries=productseries, distroseries=distroseries,
> + sourcepackagename=sourcepackagename)
> + return removeSecurityProxy(entry)
> +
> + def test_import_target_productseries(self):
> + # If the ...

review: Approve (code)
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :
Download full text (5.6 KiB)

> > === added file
> 'lib/lp/translations/browser/tests/test_translationimportqueueentry.py'
> > --- lib/lp/translations/browser/tests/test_translationimportqueueentry.py
> 1970-01-01 00:00:00 +0000
> > +++ lib/lp/translations/browser/tests/test_translationimportqueueentry.py
> 2010-02-10 13:06:21 +0000
> > @@ -0,0 +1,165 @@
> > +# Copyright 2010 Canonical Ltd. This software is licensed under the
> > +# GNU Affero General Public License version 3 (see the file LICENSE).
> > +
> > +"""Unit tests for translation import queue views."""
> > +
> > +from datetime import datetime
> > +from pytz import timezone
> > +from unittest import TestLoader
> > +
> > +from zope.component import getMultiAdapter
> > +from zope.security.proxy import removeSecurityProxy
> > +
> > +from canonical.testing import LaunchpadFunctionalLayer
> > +
> > +from canonical.launchpad.layers import TranslationsLayer, setFirstLayer
> > +from canonical.launchpad.webapp import canonical_url
> > +from canonical.launchpad.webapp.servers import LaunchpadTestRequest
> > +from lp.registry.model.sourcepackage import SourcePackage
> > +from lp.testing import TestCaseWithFactory
> > +from lp.translations.model.translationimportqueue import (
> > + TranslationImportQueue)
>
> Don't you get an warnings about this? Should test code be importing from
> model code? I thought I remember that rule (and it makes sense if we
> profess to test against Interfaces).

No warnings, but now that you mention it, I'm not sure this is allowed.
Changed.

> > +class TestTranslationImportQueueEntryView(TestCaseWithFactory):
> > + """Tests for the queue entry review form."""
> > +
> > + layer = LaunchpadFunctionalLayer
> > +
> > + def setUp(self):
> > + super(TestTranslationImportQueueEntryView, self).setUp(
> > + '<email address hidden>')
> > + self.queue = TranslationImportQueue()
>
> I think this should really be:
> self.queue = getUtility(ITranslationImportQueue)

Done.

> > + def test_import_target_sourcepackage(self):
> > + # If the entry has a DistroSeries and a SourcePackageName, the
> > + # import_target is the corresponding SourcePackage.
> > + series = self.factory.makeDistroSeries()
> > + packagename = self.factory.makeSourcePackageName()
> > + package = SourcePackage(packagename, series)
>
> package = self.factory.makeSourcePackage(packagename, series)
>
> Works just as well ... ;-)

Oh, nice! Saves me an import.

> > + # No translatable series.
> > + series_text = view.product_translatable_series
> > + self.assertEqual("Project has no translatable series.",
> series_text)
> > +
> > + # One translatable series.
> > + extra_series = self.factory.makeProductSeries(product=product)
> > + self.factory.makePOTemplate(productseries=extra_series)
> > + series_text = view.product_translatable_series
> > + self.assertIn("Project has translatable series:", series_text)
> > + # A link follows, and the sentence ends in a period.
> > + self.assertEqual('</a>.', series_text[-5:])
>
> I am not all too happy about this testing style and wonder if doing this
...

Read more...

Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

UI reviewer: the "translatable series" text now links to the individual series. Updated screenshots at http://people.canonical.com/~jtv/translationimportqueueentry-info/

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

This is fine to land. This is not the first form to but seondary, and possibly distracting information below the form. I had a minor concern that the use of un restrained left and right columns produces a large gutter of white-space between them. I do not see this since I keep my browser narrow, and U do not think you rosetta admins care.

We did discussed two webkit issues that are other bugs: the odd white space between form and buttons and the fact that this page is unreachable in webkit because the edit icons do not render (the fix is already in progress).

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

The broken icon is because there is space between the anchor and the span in translation-import-queue-macros.pt. This fixes the issue:
                    <tal:block condition="entry/required:launchpad.Admin">
                      <a class="sprite edit"
                         tal:attributes="href entry/fmt:url"><span class="invisible-link">Change
                         this entry</span></a>
                    </tal:block>

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/translations/browser/configure.zcml'
--- lib/lp/translations/browser/configure.zcml 2010-01-20 20:36:13 +0000
+++ lib/lp/translations/browser/configure.zcml 2010-02-11 13:21:26 +0000
@@ -137,6 +137,7 @@
137 permission="zope.Public"137 permission="zope.Public"
138 layer="canonical.launchpad.layers.TranslationsLayer"138 layer="canonical.launchpad.layers.TranslationsLayer"
139 name="+portlet-details"139 name="+portlet-details"
140 class="lp.translations.browser.translationimportqueue.TranslationImportQueueEntryView"
140 template="../templates/translationimportqueueentry-portlet-details.pt"/>141 template="../templates/translationimportqueueentry-portlet-details.pt"/>
141 <browser:url142 <browser:url
142 for="lp.translations.interfaces.translationimportqueue.ITranslationImportQueue"143 for="lp.translations.interfaces.translationimportqueue.ITranslationImportQueue"
143144
=== added file 'lib/lp/translations/browser/tests/test_translationimportqueueentry.py'
--- lib/lp/translations/browser/tests/test_translationimportqueueentry.py 1970-01-01 00:00:00 +0000
+++ lib/lp/translations/browser/tests/test_translationimportqueueentry.py 2010-02-11 13:21:26 +0000
@@ -0,0 +1,164 @@
1# Copyright 2010 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Unit tests for translation import queue views."""
5
6from datetime import datetime
7from pytz import timezone
8from unittest import TestLoader
9
10from zope.component import getMultiAdapter, getUtility
11from zope.security.proxy import removeSecurityProxy
12
13from canonical.testing import LaunchpadFunctionalLayer
14
15from canonical.launchpad.layers import TranslationsLayer, setFirstLayer
16from canonical.launchpad.webapp import canonical_url
17from canonical.launchpad.webapp.servers import LaunchpadTestRequest
18from lp.testing import TestCaseWithFactory
19from lp.translations.interfaces.translationimportqueue import (
20 ITranslationImportQueue)
21
22
23class TestTranslationImportQueueEntryView(TestCaseWithFactory):
24 """Tests for the queue entry review form."""
25
26 layer = LaunchpadFunctionalLayer
27
28 def setUp(self):
29 super(TestTranslationImportQueueEntryView, self).setUp(
30 'foo.bar@canonical.com')
31 self.queue = getUtility(ITranslationImportQueue)
32 self.uploader = self.factory.makePerson()
33
34 def _makeProductSeries(self):
35 """Set up a product series for a translatable product."""
36 product = self.factory.makeProduct()
37 product.official_rosetta = True
38 return product.getSeries('trunk')
39
40 def _makeView(self, entry):
41 """Create view for a queue entry."""
42 request = LaunchpadTestRequest()
43 setFirstLayer(request, TranslationsLayer)
44 view = getMultiAdapter((entry, request), name='+index')
45 view.initialize()
46 return view
47
48 def _makeEntry(self, productseries=None, distroseries=None,
49 sourcepackagename=None):
50 filename = self.factory.getUniqueString() + '.pot'
51 contents = self.factory.getUniqueString()
52 entry = self.queue.addOrUpdateEntry(
53 filename, contents, False, self.uploader,
54 productseries=productseries, distroseries=distroseries,
55 sourcepackagename=sourcepackagename)
56 return removeSecurityProxy(entry)
57
58 def test_import_target_productseries(self):
59 # If the entry's attached to a ProductSeries, that's what
60 # import_target returns.
61 series = self._makeProductSeries()
62 entry = self._makeEntry(productseries=series)
63 view = self._makeView(entry)
64
65 self.assertEqual(series, view.import_target)
66
67 def test_import_target_sourcepackage(self):
68 # If the entry has a DistroSeries and a SourcePackageName, the
69 # import_target is the corresponding SourcePackage.
70 series = self.factory.makeDistroSeries()
71 packagename = self.factory.makeSourcePackageName()
72 package = self.factory.makeSourcePackage(packagename, series)
73 entry = self._makeEntry(
74 distroseries=series, sourcepackagename=packagename)
75 view = self._makeView(entry)
76
77 self.assertEqual(package, view.import_target)
78
79 def test_productseries_templates_link(self):
80 # productseries_templates_link counts and, if appropriate links
81 # to, the series' templates.
82 series = self._makeProductSeries()
83 entry = self._makeEntry(productseries=series)
84 view = self._makeView(entry)
85
86 # If there are no templates, there is no link.
87 self.assertEqual("no templates", view.productseries_templates_link)
88
89 # For one template, there is a link. Its text uses the
90 # singular.
91 self.factory.makePOTemplate(productseries=series)
92 self.assertIn('1 template', view.productseries_templates_link)
93 self.assertNotIn('1 templates', view.productseries_templates_link)
94 url = canonical_url(series, rootsite='translations') + '/+templates'
95 self.assertIn(url, view.productseries_templates_link)
96
97 def test_product_translatable_series(self):
98 # If the entry belongs to a productseries, product_translatable_series
99 # lists the product's translatable series.
100 series = self._makeProductSeries()
101 product = series.product
102 entry = self._makeEntry(productseries=series)
103 view = self._makeView(entry)
104
105 # No translatable series.
106 series_text = view.product_translatable_series
107 self.assertEqual("Project has no translatable series.", series_text)
108
109 # One translatable series.
110 extra_series = self.factory.makeProductSeries(product=product)
111 self.factory.makePOTemplate(productseries=extra_series)
112 series_text = view.product_translatable_series
113 self.assertIn("Project has translatable series:", series_text)
114 # A link follows, and the sentence ends in a period.
115 self.assertEqual('</a>.', series_text[-5:])
116
117 # Two translatable series.
118 extra_series = self.factory.makeProductSeries(product=product)
119 self.factory.makePOTemplate(productseries=extra_series)
120 series_text = view.product_translatable_series
121 # The links to the series are separated by a comma.
122 self.assertIn("</a>, <a ", series_text)
123 # The sentence ends in a period.
124 self.assertEqual('</a>.', series_text[-5:])
125
126 # Many translatable series. The list is cut short; there's an
127 # ellipsis to indicate this.
128 series_count = len(product.translatable_series)
129 for counter in xrange(series_count, view.max_series_to_display + 1):
130 extra_series = self.factory.makeProductSeries(product=product)
131 self.factory.makePOTemplate(productseries=extra_series)
132 series_text = view.product_translatable_series
133 # The list is cut short.
134 displayed_series_count = series_text.count('</a>')
135 self.assertNotEqual(
136 len(product.translatable_series), displayed_series_count)
137 self.assertEqual(view.max_series_to_display, displayed_series_count)
138 # The list of links ends with an ellipsis.
139 self.assertEqual('</a>, ...', series_text[-9:])
140
141 def test_status_change_date(self):
142 # status_change_date describes the date of the entry's last
143 # status change.
144 series = self._makeProductSeries()
145 product = series.product
146 entry = self._makeEntry(productseries=series)
147 view = self._makeView(entry)
148
149 # If the date equals the upload date, there's no need to show
150 # anything.
151 self.assertEqual('', view.status_change_date)
152
153 # If there is a difference, there's a human-readable
154 # description.
155 UTC = timezone('UTC')
156 entry.dateimported = datetime(year=2005, month=11, day=29, tzinfo=UTC)
157 entry.date_status_changed = datetime(
158 year=2007, month=8, day=14, tzinfo=UTC)
159 self.assertEqual(
160 "Last changed on 2007-08-14.", view.status_change_date)
161
162
163def test_suite():
164 return TestLoader().loadTestsFromName(__name__)
0165
=== modified file 'lib/lp/translations/browser/translationimportqueue.py'
--- lib/lp/translations/browser/translationimportqueue.py 2010-01-13 04:46:11 +0000
+++ lib/lp/translations/browser/translationimportqueue.py 2010-02-11 13:21:26 +0000
@@ -1,7 +1,7 @@
1# Copyright 2009 Canonical Ltd. This software is licensed under the1# Copyright 2009-2010 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Browser views for ITranslationImportQueue."""4"""Browser views for `ITranslationImportQueue`."""
55
6__metaclass__ = type6__metaclass__ = type
77
@@ -21,9 +21,13 @@
21from zope.schema.vocabulary import SimpleVocabulary, SimpleTerm21from zope.schema.vocabulary import SimpleVocabulary, SimpleTerm
2222
23from canonical.database.constants import UTC_NOW23from canonical.database.constants import UTC_NOW
24from canonical.launchpad.webapp.tales import DateTimeFormatterAPI
25from canonical.launchpad.webapp.interfaces import (
26 NotFoundError, UnexpectedFormData)
24from lp.translations.browser.hastranslationimports import (27from lp.translations.browser.hastranslationimports import (
25 HasTranslationImportsView)28 HasTranslationImportsView)
26from lp.registry.interfaces.distroseries import IDistroSeries29from lp.registry.interfaces.distroseries import IDistroSeries
30from lp.registry.interfaces.sourcepackage import ISourcePackageFactory
27from lp.translations.interfaces.translationimportqueue import (31from lp.translations.interfaces.translationimportqueue import (
28 ITranslationImportQueueEntry, IEditTranslationImportQueueEntry,32 ITranslationImportQueueEntry, IEditTranslationImportQueueEntry,
29 ITranslationImportQueue, RosettaImportStatus,33 ITranslationImportQueue, RosettaImportStatus,
@@ -31,8 +35,6 @@
31from lp.services.worlddata.interfaces.language import ILanguageSet35from lp.services.worlddata.interfaces.language import ILanguageSet
32from lp.translations.interfaces.pofile import IPOFileSet36from lp.translations.interfaces.pofile import IPOFileSet
33from lp.translations.interfaces.potemplate import IPOTemplateSet37from lp.translations.interfaces.potemplate import IPOTemplateSet
34from canonical.launchpad.webapp.interfaces import (
35 NotFoundError, UnexpectedFormData)
3638
37from canonical.launchpad.webapp import (39from canonical.launchpad.webapp import (
38 action, canonical_url, GetitemNavigation, LaunchpadFormView)40 action, canonical_url, GetitemNavigation, LaunchpadFormView)
@@ -48,6 +50,8 @@
48 label = "Review import queue entry"50 label = "Review import queue entry"
49 schema = IEditTranslationImportQueueEntry51 schema = IEditTranslationImportQueueEntry
5052
53 max_series_to_display = 3
54
51 @property55 @property
52 def initial_values(self):56 def initial_values(self):
53 """Initialize some values on the form, when it's possible."""57 """Initialize some values on the form, when it's possible."""
@@ -129,6 +133,84 @@
129 return None133 return None
130134
131 @property135 @property
136 def import_target(self):
137 """The entry's `ProductSeries` or `SourcePackage`."""
138 productseries = self.context.productseries
139 distroseries = self.context.distroseries
140 sourcepackagename = self.context.sourcepackagename
141 if distroseries is None:
142 return productseries
143 else:
144 factory = getUtility(ISourcePackageFactory)
145 return factory.new(sourcepackagename, distroseries)
146
147 @property
148 def productseries_templates_link(self):
149 """Return link to `ProductSeries`' templates.
150
151 Use this only if the entry is attached to a `ProductSeries`.
152 """
153 assert self.context.productseries is not None, (
154 "Entry is not attached to a ProductSeries.")
155
156 template_count = self.context.productseries.potemplate_count
157 if template_count == 0:
158 return "no templates"
159 else:
160 link = "%s/+templates" % canonical_url(
161 self.context.productseries, rootsite='translations')
162 if template_count == 1:
163 word = "template"
164 else:
165 word = "templates"
166 return '<a href="%s">%d %s</a>' % (link, template_count, word)
167
168 def _composeProductSeriesLink(self, productseries):
169 """Produce HTML to link to `productseries`."""
170 return '<a href="%s">%s</a>' % (
171 canonical_url(productseries, rootsite='translations'),
172 productseries.name)
173
174 @property
175 def product_translatable_series(self):
176 """Summarize whether `Product` has translatable series.
177
178 Use this only if the entry is attached to a `ProductSeries`.
179 """
180 assert self.context.productseries is not None, (
181 "Entry is not attached to a ProductSeries.")
182
183 product = self.context.productseries.product
184 translatable_series = list(product.translatable_series)
185 if len(translatable_series) == 0:
186 return "Project has no translatable series."
187 else:
188 links = [
189 self._composeProductSeriesLink(series)
190 for series in translatable_series[:self.max_series_to_display]
191 ]
192 links_text = ', '.join(links)
193 if len(translatable_series) > self.max_series_to_display:
194 tail = ", ..."
195 else:
196 tail = "."
197 return "Project has translatable series: " + links_text + tail
198
199 @property
200 def status_change_date(self):
201 """Show date of last status change.
202
203 Says nothing at all if the entry's status has not changed since
204 upload.
205 """
206 change_date = self.context.date_status_changed
207 if change_date == self.context.dateimported:
208 return ""
209 else:
210 formatter = DateTimeFormatterAPI(change_date)
211 return "Last changed %s." % formatter.displaydate()
212
213 @property
132 def next_url(self):214 def next_url(self):
133 """See `LaunchpadFormView`."""215 """See `LaunchpadFormView`."""
134 # The referer header we want is only available before the view's216 # The referer header we want is only available before the view's
135217
=== added file 'lib/lp/translations/stories/importqueue/xx-entry-details.txt'
--- lib/lp/translations/stories/importqueue/xx-entry-details.txt 1970-01-01 00:00:00 +0000
+++ lib/lp/translations/stories/importqueue/xx-entry-details.txt 2010-02-11 13:21:26 +0000
@@ -0,0 +1,106 @@
1= Entry details =
2
3The translation import queue entry page shows various details about an
4entry and its target that may be helpful in queue review.
5
6 >>> from zope.security.proxy import removeSecurityProxy
7 >>> from lp.translations.model.translationimportqueue import (
8 ... TranslationImportQueue)
9
10 >>> filename = 'po/foo.pot'
11
12 >>> login(ANONYMOUS)
13 >>> queue = TranslationImportQueue()
14 >>> product = factory.makeProduct()
15 >>> removeSecurityProxy(product).official_rosetta = True
16 >>> trunk = product.getSeries('trunk')
17 >>> uploader = factory.makePerson()
18 >>> entry = queue.addOrUpdateEntry(
19 ... filename, '# empty', False, uploader, productseries=trunk)
20 >>> entry_url = canonical_url(entry, rootsite='translations')
21 >>> logout()
22
23 >>> admin_browser.open(entry_url)
24 >>> details = find_tag_by_id(admin_browser.contents, 'portlet-details')
25 >>> details_text = extract_text(details)
26 >>> print details_text
27 Upload attached to ... trunk series.
28 This project...s license is open source.
29 Release series has no templates.
30 Project has no translatable series.
31 File po/foo.pot uploaded by ...
32
33The details include the project the entry is for, and who uploaded it.
34
35 >>> product.displayname in details_text
36 True
37 >>> uploader.displayname in details_text
38 True
39
40There's also a link to the file's contents.
41
42 >>> print admin_browser.getLink(filename).text
43 po/foo.pot
44 >>> print admin_browser.getLink(filename).url
45 http://...foo.pot
46
47
48== Existing templates ==
49
50If there are translatable templates in the series, this will be stated
51and there will be a link to the templates list.
52
53 >>> login(ANONYMOUS)
54 >>> template = factory.makePOTemplate(productseries=trunk)
55 >>> logout()
56
57 >>> admin_browser.open(entry_url)
58 >>> details = find_tag_by_id(admin_browser.contents, 'portlet-details')
59 >>> details_text = extract_text(details)
60 >>> print details_text
61 Upload attached to
62 ...
63 Release series has 1 template.
64 ...
65
66 >>> print admin_browser.getLink('1 template').url
67 http...://translations.launchpad.dev/.../trunk/+templates
68
69 >>> admin_browser.getLink('1 template').click()
70 >>> print admin_browser.title
71 All templates : Translations : Series trunk : ...
72
73In that case, the product is also shown to have translatable series.
74
75 >>> print details_text
76 Upload attached to
77 ...
78 Project has translatable series: trunk.
79 ...
80
81
82== Source packages ==
83
84The portlet shows different (well, less) information for uploads
85attached to distribution packages.
86
87 >>> from lp.registry.model.distribution import DistributionSet
88
89 >>> login(ANONYMOUS)
90 >>> distro = DistributionSet().getByName('ubuntu')
91 >>> distroseries = distro.getSeries('hoary')
92 >>> packagename = factory.makeSourcePackageName(name='xpad')
93 >>> entry = queue.addOrUpdateEntry(
94 ... filename, '# nothing', True, uploader, distroseries=distroseries,
95 ... sourcepackagename=packagename)
96 >>> entry_url = canonical_url(entry, rootsite='translations')
97 >>> logout()
98
99This only shows the source package and the file.
100
101 >>> admin_browser.open(entry_url)
102 >>> details = find_tag_by_id(admin_browser.contents, 'portlet-details')
103 >>> details_text = extract_text(details)
104 >>> print details_text
105 Upload attached to xpad in Ubuntu Hoary.
106 File po/foo.pot uploaded by ...
0107
=== added file 'lib/lp/translations/stories/importqueue/xx-entry-error-output.txt'
--- lib/lp/translations/stories/importqueue/xx-entry-error-output.txt 1970-01-01 00:00:00 +0000
+++ lib/lp/translations/stories/importqueue/xx-entry-error-output.txt 2010-02-11 13:21:26 +0000
@@ -0,0 +1,46 @@
1= Translation import queue entry error output =
2
3The approval form translation import queue entries shows any error
4or warning output that the entry may have incurred.
5
6By default, this is nothing.
7
8 >>> from lp.translations.model.translationimportqueue import (
9 ... TranslationImportQueue)
10
11 >>> def find_error_output(browser):
12 ... """Find error-output section on page."""
13 ... return find_tag_by_id(browser.contents, 'error-output')
14
15 >>> login(ANONYMOUS)
16 >>> queue = TranslationImportQueue()
17 >>> product = factory.makeProduct()
18 >>> trunk = product.getSeries('trunk')
19 >>> entry = queue.addOrUpdateEntry(
20 ... 'la.po', '# contents', False, product.owner, productseries=trunk)
21 >>> entry_url = canonical_url(entry, rootsite='translations')
22 >>> logout()
23
24 >>> admin_browser.open(entry_url)
25 >>> output_panel = find_error_output(admin_browser)
26 >>> print output_panel
27 None
28
29The section showing the output only shows up when there is output to
30show.
31
32 >>> entry.error_output = "Things went horribly wrong."
33 >>> admin_browser.open(entry_url)
34 >>> output_panel = find_error_output(admin_browser)
35 >>> print extract_text(output_panel)
36 Error output for this entry:
37 Things went horribly wrong.
38
39The output is properly HTML-escaped, so is safe to display in this way.
40
41 >>> entry.error_output = "<h1>Injection &amp; subterfuge</h1>"
42 >>> admin_browser.open(entry_url)
43 >>> output_panel = find_error_output(admin_browser)
44 >>> print output_panel.renderContents()
45 Error output for this entry:
46 ...&lt;h1&gt;Injection &amp;amp; subterfuge&lt;/h1&gt;...
047
=== modified file 'lib/lp/translations/templates/translationimportqueueentry-index.pt'
--- lib/lp/translations/templates/translationimportqueueentry-index.pt 2009-12-03 18:33:22 +0000
+++ lib/lp/translations/templates/translationimportqueueentry-index.pt 2010-02-11 13:21:26 +0000
@@ -106,6 +106,19 @@
106 tal:attributes="value view/referrer_url" />106 tal:attributes="value view/referrer_url" />
107 </div>107 </div>
108 </div>108 </div>
109
110 <tal:details replace="structure context/@@+portlet-details" />
111
112 <div tal:condition="context/error_output"
113 class="portlet"
114 id="error-output">
115 Error output for this entry:
116 <br />
117 <tt tal:content="context/error_output">
118 Horrible syntax error near line 192.
119 </tt>
120 </div>
121
109 </div>122 </div>
110123
111 </body>124 </body>
112125
=== modified file 'lib/lp/translations/templates/translationimportqueueentry-portlet-details.pt'
--- lib/lp/translations/templates/translationimportqueueentry-portlet-details.pt 2009-07-17 17:59:07 +0000
+++ lib/lp/translations/templates/translationimportqueueentry-portlet-details.pt 2010-02-11 13:21:26 +0000
@@ -4,41 +4,48 @@
4 xmlns:i18n="http://xml.zope.org/namespaces/i18n"4 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
5 omit-tag="">5 omit-tag="">
66
7<div class="portlet" id="portlet-details">7<div class="columns portlet" id="portlet-details">
8 <h2>Import entry details</h2>8 <div class="two column left">
99 Upload attached to
10 <div class="portletBody">10 <a tal:replace="structure view/import_target/fmt:link">
11 <div class="portletContent">11 Evolution in Ubuntu Hoary</a>.
12 <b>Origin:</b><br />12
13 <a tal:condition="context/sourcepackage"13 <tal:productseries condition="context/productseries">
14 tal:content="context/sourcepackage/title"14 <ul class="bulleted" tal:define="product context/productseries/product">
15 tal:attributes="href string:${context/sourcepackage/fmt:url}"15 <li>
16 >evolution in Ubuntu Hoary</a>16 <tal:license
17 <a tal:condition="context/productseries"17 replace="structure product/license_status/description">
18 tal:content="context/productseries/title"18 This project's license is open source.
19 tal:attributes="href string:${context/productseries/fmt:url}"19 </tal:license>
20 >Evolution Series: MAIN</a><br />20 </li>
21 <b>Path:</b>21 <li>
22 <a tal:attributes="href context/content/http_url"22 Release series has
23 tal:content="context/path">po/foo.pot</a><br />23 <tal:templates replace="structure view/productseries_templates_link">
24 <b>Uploader:</b><br />24 2 templates</tal:templates>.
25 <a tal:replace="structure context/importer/fmt:link"25 </li>
26 >Mark Shuttleworth</a><br />26 <li>
27 <b>Waiting since:</b>27 <tal:series replace="structure view/product_translatable_series">
28 <span28 Project has translatable series: trunk, 0.1, 0.2, ...
29 tal:attributes="title context/dateimported/fmt:datetime"29 </tal:series>
30 tal:content="context/dateimported/fmt:approximatedate">30 </li>
31 2004-05-2231 </ul>
32 </span><br />32 </tal:productseries>
33 <b>Status:</b>33 </div>
34 <span tal:replace="context/status/title"></span><br />34
35 <b>Status changed:</b>35 <div class="two column right">
36 <span36 File
37 tal:attributes="title context/date_status_changed/fmt:datetime"37 <a tal:attributes="href context/content/http_url"
38 tal:content="context/date_status_changed/fmt:approximatedate">38 tal:content="context/path">po/messages.pot</a>
39 2004-05-2139 uploaded by
40 </span><br />40 <a tal:replace="structure context/importer/fmt:link">
41 </div>41 Arne Goetje
42 </div>42 </a>
43 <tal:upload_date replace="context/dateimported/fmt:displaydate">
44 2010-02-10
45 </tal:upload_date>.
46 <tal:status_change replace="view/status_change_date">
47 Entry last changed on 2010-02-12.
48 </tal:status_change>
49 </div>
43</div>50</div>
44</tal:root>51</tal:root>