Merge lp:~jtv/launchpad/bug-517700 into lp:launchpad

Proposed by Jeroen T. Vermeulen
Status: Merged
Approved by: Jeroen T. Vermeulen
Approved revision: no longer in the source branch.
Merged at revision: 11464
Proposed branch: lp:~jtv/launchpad/bug-517700
Merge into: lp:launchpad
Diff against target: 795 lines (+456/-144)
7 files modified
lib/canonical/launchpad/doc/launchpad-views-cookie.txt (+2/-2)
lib/lp/translations/browser/pofile.py (+160/-90)
lib/lp/translations/browser/tests/test_pofile_view.py (+259/-10)
lib/lp/translations/interfaces/translationsperson.py (+3/-0)
lib/lp/translations/model/translationsperson.py (+4/-0)
lib/lp/translations/templates/pofile-translate.pt (+1/-42)
lib/lp/translations/tests/test_translationsperson.py (+27/-0)
To merge this branch: bzr merge lp:~jtv/launchpad/bug-517700
Reviewer Review Type Date Requested Status
Abel Deuring (community) code Approve
Review via email: mp+33888@code.launchpad.net

Commit message

+translate "bubble help" for new translators

Description of the change

= Bugs 484375, 517700 =

As sketched out by Matthew Revell and others, this adds something to the "help bubble" that we show on translation pages that have documentation worthy of the user's attention.

The part that is added is a link to introductory documentation. This is added on top of the existing links for a translation group's guidelines and a translation team's style guide.

No changes in interaction were needed, apart from the bubble now also being shown if the user is logged in but has never translated. The changes may look bigger than they are because I lifted the bewildering bubble fragment out of the TAL and moved it into the browser code. Easier to read, easier to test, faster. There's also a pagetest, but it passes unmodified (yay!).

In the browser code, I factored out a bunch of properties that were common to two view classes. At first I thought one of these view classes was scheduled to replace the other, justifying some temporary duplication, but according to the docstring it's actually set to replace a _different_ set of view classes. So I eliminated the duplication.

The view test was running in LaunchpadZopeless layer, but had no need for either the Librarian or memcached so I downgraded it to ZopelessDatabaseLayer. You'll notice that I run the exact same tests against both view classes that are based on the new mixin I factored out. That may be overkill, or it may be comforting; you decide.

Jeroen

To post a comment you must log in.
Revision history for this message
Abel Deuring (adeuring) wrote :

nice work!

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/canonical/launchpad/doc/launchpad-views-cookie.txt'
--- lib/canonical/launchpad/doc/launchpad-views-cookie.txt 2009-03-06 19:09:45 +0000
+++ lib/canonical/launchpad/doc/launchpad-views-cookie.txt 2010-08-27 10:57:43 +0000
@@ -35,7 +35,7 @@
35 >>> launchpad_views['small_maps']35 >>> launchpad_views['small_maps']
36 False36 False
3737
38Any other value is treated as True because that is default state.38Any other value is treated as True because that is the default state.
3939
40 >>> launchpad_views = test_get_launchpad_views(40 >>> launchpad_views = test_get_launchpad_views(
41 ... 'launchpad_views=small_maps=true')41 ... 'launchpad_views=small_maps=true')
@@ -47,7 +47,7 @@
47 >>> launchpad_views['small_maps']47 >>> launchpad_views['small_maps']
48 True48 True
4949
50Keys that are note predefined in get_launchpad_views are not accepted.50Keys that are not predefined in get_launchpad_views are not accepted.
5151
52 >>> launchpad_views = test_get_launchpad_views(52 >>> launchpad_views = test_get_launchpad_views(
53 ... 'launchpad_views=bad_key=false')53 ... 'launchpad_views=bad_key=false')
5454
=== modified file 'lib/lp/translations/browser/pofile.py'
--- lib/lp/translations/browser/pofile.py 2010-08-20 20:31:18 +0000
+++ lib/lp/translations/browser/pofile.py 2010-08-27 10:57:43 +0000
@@ -1,4 +1,4 @@
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 code for Translation files."""4"""Browser code for Translation files."""
@@ -16,17 +16,18 @@
16 'POFileView',16 'POFileView',
17 ]17 ]
1818
19from cgi import escape
19import os.path20import os.path
20import re21import re
21import urllib22import urllib
2223
23from zope.app.form.browser import DropdownWidget
24from zope.component import getUtility24from zope.component import getUtility
25from zope.publisher.browser import FileUpload25from zope.publisher.browser import FileUpload
2626
27from canonical.cachedproperty import cachedproperty27from canonical.cachedproperty import cachedproperty
28from canonical.config import config28from canonical.config import config
29from canonical.launchpad import _29from canonical.launchpad import _
30from canonical.launchpad.interfaces import ILaunchBag
30from canonical.launchpad.webapp import (31from canonical.launchpad.webapp import (
31 canonical_url,32 canonical_url,
32 enabled_with_permission,33 enabled_with_permission,
@@ -59,12 +60,6 @@
59from lp.translations.interfaces.translationsperson import ITranslationsPerson60from lp.translations.interfaces.translationsperson import ITranslationsPerson
6061
6162
62class CustomDropdownWidget(DropdownWidget):
63 def _div(self, cssClass, contents, **kw):
64 """Render the select widget without the div tag."""
65 return contents
66
67
68class POFileNavigation(Navigation):63class POFileNavigation(Navigation):
6964
70 usedfor = IPOFile65 usedfor = IPOFile
@@ -143,7 +138,149 @@
143 links = ('details', 'translate', 'upload', 'download')138 links = ('details', 'translate', 'upload', 'download')
144139
145140
146class POFileBaseView(LaunchpadView):141class POFileMetadataViewMixin:
142 """`POFile` metadata that multiple views can use."""
143
144 @cachedproperty
145 def translation_group(self):
146 """Is there a translation group for this translation?
147
148 :return: TranslationGroup or None if not found.
149 """
150 translation_groups = self.context.potemplate.translationgroups
151 if translation_groups is not None and len(translation_groups) > 0:
152 group = translation_groups[0]
153 else:
154 group = None
155 return group
156
157 @cachedproperty
158 def translator_entry(self):
159 """The translator entry or None if none is assigned."""
160 group = self.translation_group
161 if group is not None:
162 return group.query_translator(self.context.language)
163 return None
164
165 @cachedproperty
166 def translator(self):
167 """Who is assigned for translations to this language?"""
168 translator_entry = self.translator_entry
169 if translator_entry is not None:
170 return translator_entry.translator
171 return None
172
173 @cachedproperty
174 def user_is_new_translator(self):
175 """Is this user someone who has done no translation work yet?"""
176 user = getUtility(ILaunchBag).user
177 if user is not None:
178 translationsperson = ITranslationsPerson(user)
179 if not translationsperson.hasTranslated():
180 return True
181
182 return False
183
184 @cachedproperty
185 def translation_group_guide(self):
186 """URL to translation group's translation guide, if any."""
187 group = self.translation_group
188 if group is None:
189 return None
190 else:
191 return group.translation_guide_url
192
193 @cachedproperty
194 def translation_team_guide(self):
195 """URL to translation team's translation guide, if any."""
196 translator = self.translator_entry
197 if translator is None:
198 return None
199 else:
200 return translator.style_guide_url
201
202 @cachedproperty
203 def has_any_documentation(self):
204 """Return whether there is any documentation for this POFile."""
205 return (
206 self.translation_group_guide is not None or
207 self.translation_team_guide is not None or
208 self.user_is_new_translator)
209
210 @property
211 def introduction_link(self):
212 """Link to introductory documentation, if appropriate.
213
214 If no link is appropriate, returns the empty string.
215 """
216 if not self.user_is_new_translator:
217 return ""
218
219 return """
220 New to translating in Launchpad?
221 <a href="/+help/new-to-translating.html" target="help">
222 Read our guide</a>.
223 """
224
225 @property
226 def guide_links(self):
227 """Links to translation group/team guidelines, if available.
228
229 If no guidelines are available, returns the empty string.
230 """
231 group_guide = self.translation_group_guide
232 team_guide = self.translation_team_guide
233 if group_guide is None and team_guide is None:
234 return ""
235
236 links = []
237 if group_guide is not None:
238 links.append("""
239 <a class="style-guide-url" href="%s">%s instructions</a>
240 """ % (group_guide, escape(self.translation_group.title)))
241
242 if team_guide is not None:
243 if group_guide is None:
244 # Use team's full name.
245 name = self.translator.displayname
246 else:
247 # Full team name may get tedious after we just named the
248 # group. Just use the language name.
249 name = self.context.language.englishname
250 links.append("""
251 <a class="style-guide-url" href="%s"> %s guidelines</a>
252 """ % (team_guide, escape(name)))
253
254 text = ' and '.join(links).rstrip()
255
256 return "Before translating, be sure to go through %s." % text
257
258 @property
259 def documentation_link_bubble(self):
260 """Reference to documentation, if appopriate."""
261 if not self.has_any_documentation:
262 return ""
263
264 return """
265 <div class="important-notice-container">
266 <div class="important-notice-balloon">
267 <div class="important-notice-buttons">
268 <img class="important-notice-cancel-button"
269 src="/@@/no"
270 alt="Don't show this notice anymore"
271 title="Hide this notice." />
272 </div>
273 <span class="sprite info">
274 <span class="important-notice">
275 %s
276 </span>
277 </div>
278 </div>
279 """ % ' '.join([
280 self.introduction_link, self.guide_links])
281
282
283class POFileBaseView(LaunchpadView, POFileMetadataViewMixin):
147 """A basic view for a POFile284 """A basic view for a POFile
148285
149 This view is different from POFileView as it is the base for a new286 This view is different from POFileView as it is the base for a new
@@ -161,7 +298,6 @@
161298
162 self.batchnav = self._buildBatchNavigator()299 self.batchnav = self._buildBatchNavigator()
163300
164
165 @cachedproperty301 @cachedproperty
166 def contributors(self):302 def contributors(self):
167 return tuple(self.context.contributors)303 return tuple(self.context.contributors)
@@ -250,46 +386,6 @@
250 return self.context.language.pluralexpression386 return self.context.language.pluralexpression
251 return ""387 return ""
252388
253 @cachedproperty
254 def translation_group(self):
255 """Is there a translation group for this translation?
256
257 :return: TranslationGroup or None if not found.
258 """
259 translation_groups = self.context.potemplate.translationgroups
260 if translation_groups is not None and len(translation_groups) > 0:
261 group = translation_groups[0]
262 else:
263 group = None
264 return group
265
266 def _get_translator_entry(self):
267 """The translator entry or None if none is assigned."""
268 group = self.translation_group
269 if group is not None:
270 return group.query_translator(self.context.language)
271 return None
272
273 @cachedproperty
274 def translator(self):
275 """Who is assigned for translations to this language?"""
276 translator_entry = self._get_translator_entry()
277 if translator_entry is not None:
278 return translator_entry.translator
279 return None
280
281 @cachedproperty
282 def has_any_documentation(self):
283 """Return whether there is any documentation for this POFile."""
284 if (self.translation_group is not None and
285 self.translation_group.translation_guide_url is not None):
286 return True
287 translator_entry = self._get_translator_entry()
288 if (translator_entry is not None and
289 translator_entry.style_guide_url is not None):
290 return True
291 return False
292
293 def _initializeShowOption(self):389 def _initializeShowOption(self):
294 # Get any value given by the user390 # Get any value given by the user
295 self.show = self.request.form_ng.getOne('show')391 self.show = self.request.form_ng.getOne('show')
@@ -462,6 +558,12 @@
462558
463559
464class TranslationMessageContainer:560class TranslationMessageContainer:
561 """A `TranslationMessage` decorated with usage class.
562
563 The usage class (in-use, hidden" or suggested) is used in CSS to
564 render these messages differently.
565 """
566
465 def __init__(self, translation, pofile):567 def __init__(self, translation, pofile):
466 self.data = translation568 self.data = translation
467569
@@ -478,6 +580,8 @@
478580
479581
480class FilteredPOTMsgSets:582class FilteredPOTMsgSets:
583 """`POTMsgSet`s and translations shown by the `POFileFilteredView`."""
584
481 def __init__(self, translations, pofile):585 def __init__(self, translations, pofile):
482 potmsgsets = []586 potmsgsets = []
483 current_potmsgset = None587 current_potmsgset = None
@@ -494,10 +598,10 @@
494 potmsgsets.append(current_potmsgset)598 potmsgsets.append(current_potmsgset)
495 translation.setPOFile(pofile)599 translation.setPOFile(pofile)
496 current_potmsgset = {600 current_potmsgset = {
497 'potmsgset' : translation.potmsgset,601 'potmsgset': translation.potmsgset,
498 'translations' : [TranslationMessageContainer(602 'translations': [
499 translation, pofile)],603 TranslationMessageContainer(translation, pofile)],
500 'context' : translation604 'context': translation,
501 }605 }
502 if current_potmsgset is not None:606 if current_potmsgset is not None:
503 potmsgsets.append(current_potmsgset)607 potmsgsets.append(current_potmsgset)
@@ -523,7 +627,7 @@
523 """See `LaunchpadView`."""627 """See `LaunchpadView`."""
524 return smartquote('Translations by %s in "%s"') % (628 return smartquote('Translations by %s in "%s"') % (
525 self._person_name, self.context.title)629 self._person_name, self.context.title)
526 630
527 def label(self):631 def label(self):
528 """See `LaunchpadView`."""632 """See `LaunchpadView`."""
529 return "Translations by %s" % self._person_name633 return "Translations by %s" % self._person_name
@@ -663,7 +767,7 @@
663 return config.rosetta.translate_pages_max_batch_size767 return config.rosetta.translate_pages_max_batch_size
664768
665769
666class POFileTranslateView(BaseTranslationView):770class POFileTranslateView(BaseTranslationView, POFileMetadataViewMixin):
667 """The View class for a `POFile` or a `DummyPOFile`.771 """The View class for a `POFile` or a `DummyPOFile`.
668772
669 This view is based on `BaseTranslationView` and implements the API773 This view is based on `BaseTranslationView` and implements the API
@@ -711,40 +815,6 @@
711 # BaseTranslationView API815 # BaseTranslationView API
712 #816 #
713817
714 @cachedproperty
715 def translation_group(self):
716 """Is there a translation group for this translation?
717
718 :return: TranslationGroup or None if not found.
719 """
720 translation_groups = self.context.potemplate.translationgroups
721 if translation_groups is not None and len(translation_groups) > 0:
722 group = translation_groups[0]
723 else:
724 group = None
725 return group
726
727 @cachedproperty
728 def translation_team(self):
729 """Is there a translation group for this translation."""
730 group = self.translation_group
731 if group is not None:
732 team = group.query_translator(self.context.language)
733 else:
734 team = None
735 return team
736
737 @cachedproperty
738 def has_any_documentation(self):
739 """Return whether there is any documentation for this POFile."""
740 if (self.translation_group is not None and
741 self.translation_group.translation_guide_url is not None):
742 return True
743 if (self.translation_team is not None and
744 self.translation_team.style_guide_url is not None):
745 return True
746 return False
747
748 def _buildBatchNavigator(self):818 def _buildBatchNavigator(self):
749 """See BaseTranslationView._buildBatchNavigator."""819 """See BaseTranslationView._buildBatchNavigator."""
750820
751821
=== modified file 'lib/lp/translations/browser/tests/test_pofile_view.py'
--- lib/lp/translations/browser/tests/test_pofile_view.py 2010-08-20 20:31:18 +0000
+++ lib/lp/translations/browser/tests/test_pofile_view.py 2010-08-27 10:57:43 +0000
@@ -1,4 +1,4 @@
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__metaclass__ = type4__metaclass__ = type
@@ -7,14 +7,16 @@
7 datetime,7 datetime,
8 timedelta,8 timedelta,
9 )9 )
10from unittest import TestLoader
1110
12import pytz11import pytz
1312
14from canonical.launchpad.webapp.servers import LaunchpadTestRequest13from canonical.launchpad.webapp.servers import LaunchpadTestRequest
15from canonical.testing import LaunchpadZopelessLayer14from canonical.testing import ZopelessDatabaseLayer
16from lp.app.errors import UnexpectedFormData15from lp.app.errors import UnexpectedFormData
17from lp.testing import TestCaseWithFactory16from lp.testing import (
17 login,
18 TestCaseWithFactory,
19 )
18from lp.translations.browser.pofile import (20from lp.translations.browser.pofile import (
19 POFileBaseView,21 POFileBaseView,
20 POFileTranslateView,22 POFileTranslateView,
@@ -24,7 +26,7 @@
24class TestPOFileBaseViewFiltering(TestCaseWithFactory):26class TestPOFileBaseViewFiltering(TestCaseWithFactory):
25 """Test POFileBaseView filtering functions."""27 """Test POFileBaseView filtering functions."""
2628
27 layer = LaunchpadZopelessLayer29 layer = ZopelessDatabaseLayer
2830
29 def gen_now(self):31 def gen_now(self):
30 now = datetime.now(pytz.UTC)32 now = datetime.now(pytz.UTC)
@@ -161,7 +163,7 @@
161class TestPOFileBaseViewInvalidFiltering(TestCaseWithFactory,163class TestPOFileBaseViewInvalidFiltering(TestCaseWithFactory,
162 TestInvalidFilteringMixin):164 TestInvalidFilteringMixin):
163 """Test for POFilleBaseView."""165 """Test for POFilleBaseView."""
164 layer = LaunchpadZopelessLayer166 layer = ZopelessDatabaseLayer
165 view_class = POFileBaseView167 view_class = POFileBaseView
166168
167 def setUp(self):169 def setUp(self):
@@ -172,7 +174,7 @@
172class TestPOFileTranslateViewInvalidFiltering(TestCaseWithFactory,174class TestPOFileTranslateViewInvalidFiltering(TestCaseWithFactory,
173 TestInvalidFilteringMixin):175 TestInvalidFilteringMixin):
174 """Test for POFilleTranslateView."""176 """Test for POFilleTranslateView."""
175 layer = LaunchpadZopelessLayer177 layer = ZopelessDatabaseLayer
176 view_class = POFileTranslateView178 view_class = POFileTranslateView
177179
178 def setUp(self):180 def setUp(self):
@@ -180,6 +182,253 @@
180 self.pofile = self.factory.makePOFile('eo')182 self.pofile = self.factory.makePOFile('eo')
181183
182184
183def test_suite():185class DocumentationScenarioMixin:
184 return TestLoader().loadTestsFromName(__name__)186 """Tests for `POFileBaseView` and `POFileTranslateView`."""
185187 # The view class that's being tested.
188 view_class = None
189
190 def _makeLoggedInUser(self):
191 """Create a user, and log in as that user."""
192 email = self.factory.getUniqueString() + '@example.com'
193 user = self.factory.makePerson(email=email)
194 login(email)
195 return user
196
197 def _useNonnewTranslator(self):
198 """Create a user who's done translations, and log in as that user."""
199 user = self._makeLoggedInUser()
200 self.factory.makeSharedTranslationMessage(
201 translator=user, suggestion=True)
202 return user
203
204 def _makeView(self, pofile=None, request=None):
205 """Create a view of type `view_class`.
206
207 :param pofile: An optional `POFile`. If not given, one will be
208 created.
209 :param request: An optional `LaunchpadTestRequest`. If not
210 given, one will be created.
211 """
212 if pofile is None:
213 pofile = self.factory.makePOFile('cy')
214 if request is None:
215 request = LaunchpadTestRequest()
216 return self.view_class(pofile, request)
217
218 def _makeTranslationGroup(self, pofile):
219 """Set up a translation group for pofile if it doesn't have one."""
220 product = pofile.potemplate.productseries.product
221 if product.translationgroup is None:
222 product.translationgroup = self.factory.makeTranslationGroup()
223 return product.translationgroup
224
225 def _makeTranslationTeam(self, pofile):
226 """Create a translation team applying to pofile."""
227 language = pofile.language.code
228 group = self._makeTranslationGroup(pofile)
229 return self.factory.makeTranslator(language, group=group)
230
231 def _setGroupGuide(self, pofile):
232 """Set the translation group guide URL for pofile."""
233 guide = "http://%s.example.com/" % self.factory.getUniqueString()
234 self._makeTranslationGroup(pofile).translation_guide_url = guide
235 return guide
236
237 def _setTeamGuide(self, pofile, team=None):
238 """Set the translation team style guide URL for pofile."""
239 guide = "http://%s.example.com/" % self.factory.getUniqueString()
240 if team is None:
241 team = self._makeTranslationTeam(pofile)
242 team.style_guide_url = guide
243 return guide
244
245 def _showsIntro(self, bubble_text):
246 """Does bubble_text show the intro for new translators?"""
247 return "New to translating in Launchpad?" in bubble_text
248
249 def _showsGuides(self, bubble_text):
250 """Does bubble_text show translation group/team guidelines?"""
251 return "Before translating" in bubble_text
252
253 def test_user_is_new_translator_anonymous(self):
254 # An anonymous user is not a new translator.
255 self.assertFalse(self._makeView().user_is_new_translator)
256
257 def test_user_is_new_translator_new(self):
258 # A user who's never done any translations is a new translator.
259 self._makeLoggedInUser()
260 self.assertTrue(self._makeView().user_is_new_translator)
261
262 def test_user_is_new_translator_not_new(self):
263 # A user who has done translations is not a new translator.
264 self._useNonnewTranslator()
265 self.assertFalse(self._makeView().user_is_new_translator)
266
267 def test_translation_group_guide_nogroup(self):
268 # If there's no translation group, there is no
269 # translation_group_guide.
270 self.assertIs(None, self._makeView().translation_group_guide)
271
272 def test_translation_group_guide_noguide(self):
273 # The translation group may not have a translation guide.
274 pofile = self.factory.makePOFile('ca')
275 self._makeTranslationGroup(pofile)
276
277 view = self._makeView(pofile=pofile)
278 self.assertIs(None, view.translation_group_guide)
279
280 def test_translation_group_guide(self):
281 # translation_group_guide returns the translation group's style
282 # guide URL if there is one.
283 pofile = self.factory.makePOFile('ce')
284 url = self._setGroupGuide(pofile)
285
286 view = self._makeView(pofile=pofile)
287 self.assertEqual(url, view.translation_group_guide)
288
289 def test_translation_team_guide_nogroup(self):
290 # If there is no translation group, there is no translation team
291 # style guide.
292 self.assertIs(None, self._makeView().translation_team_guide)
293
294 def test_translation_team_guide_noteam(self):
295 # If there is no translation team for this language, there is on
296 # translation team style guide.
297 pofile = self.factory.makePOFile('ch')
298 self._makeTranslationGroup(pofile)
299
300 view = self._makeView(pofile=pofile)
301 self.assertIs(None, view.translation_team_guide)
302
303 def test_translation_team_guide_noguide(self):
304 # A translation team may not have a translation style guide.
305 pofile = self.factory.makePOFile('co')
306 self._makeTranslationTeam(pofile)
307
308 view = self._makeView(pofile=pofile)
309 self.assertIs(None, view.translation_team_guide)
310
311 def test_translation_team_guide(self):
312 # translation_team_guide returns the translation team's
313 # style guide, if there is one.
314 pofile = self.factory.makePOFile('cy')
315 url = self._setTeamGuide(pofile)
316
317 view = self._makeView(pofile=pofile)
318 self.assertEqual(url, view.translation_team_guide)
319
320 def test_documentation_link_bubble_empty(self):
321 # If the user is not a new translator and neither a translation
322 # group nor a team style guide applies, the documentation bubble
323 # is empty.
324 pofile = self.factory.makePOFile('da')
325 self._useNonnewTranslator()
326
327 view = self._makeView(pofile=pofile)
328 self.assertEqual('', view.documentation_link_bubble)
329 self.assertFalse(self._showsIntro(view.documentation_link_bubble))
330 self.assertFalse(self._showsGuides(view.documentation_link_bubble))
331
332 def test_documentation_link_bubble_intro(self):
333 # New users are shown an intro link.
334 self._makeLoggedInUser()
335
336 view = self._makeView()
337 self.assertTrue(self._showsIntro(view.documentation_link_bubble))
338 self.assertFalse(self._showsGuides(view.documentation_link_bubble))
339
340 def test_documentation_link_bubble_group_guide(self):
341 # A translation group's guide shows up in the documentation
342 # bubble.
343 pofile = self.factory.makePOFile('de')
344 self._setGroupGuide(pofile)
345
346 view = self._makeView(pofile=pofile)
347 self.assertFalse(self._showsIntro(view.documentation_link_bubble))
348 self.assertTrue(self._showsGuides(view.documentation_link_bubble))
349
350 def test_documentation_link_bubble_team_guide(self):
351 # A translation team's style guide shows up in the documentation
352 # bubble.
353 pofile = self.factory.makePOFile('de')
354 self._setTeamGuide(pofile)
355
356 view = self._makeView(pofile=pofile)
357 self.assertFalse(self._showsIntro(view.documentation_link_bubble))
358 self.assertTrue(self._showsGuides(view.documentation_link_bubble))
359
360 def test_documentation_link_bubble_both_guides(self):
361 # The documentation bubble can show both a translation group's
362 # guidelines and a translation team's style guide.
363 pofile = self.factory.makePOFile('dv')
364 self._setGroupGuide(pofile)
365 self._setTeamGuide(pofile)
366
367 view = self._makeView(pofile=pofile)
368 self.assertFalse(self._showsIntro(view.documentation_link_bubble))
369 self.assertTrue(self._showsGuides(view.documentation_link_bubble))
370 self.assertIn(" and ", view.documentation_link_bubble)
371
372 def test_documentation_link_bubble_shows_all(self):
373 # So in all, the bubble can show 3 different documentation
374 # links.
375 pofile = self.factory.makePOFile('dz')
376 self._makeLoggedInUser()
377 self._setGroupGuide(pofile)
378 self._setTeamGuide(pofile)
379
380 view = self._makeView(pofile=pofile)
381 self.assertTrue(self._showsIntro(view.documentation_link_bubble))
382 self.assertTrue(self._showsGuides(view.documentation_link_bubble))
383 self.assertIn(" and ", view.documentation_link_bubble)
384
385 def test_documentation_link_bubble_escapes_group_title(self):
386 # Translation group titles in the bubble are HTML-escaped.
387 pofile = self.factory.makePOFile('eo')
388 group = self._makeTranslationGroup(pofile)
389 self._setGroupGuide(pofile)
390 group.title = "<blink>X</blink>"
391
392 view = self._makeView(pofile=pofile)
393 self.assertIn(
394 "&lt;blink&gt;X&lt;/blink&gt;", view.documentation_link_bubble)
395 self.assertNotIn(group.title, view.documentation_link_bubble)
396
397 def test_documentation_link_bubble_escapes_team_name(self):
398 # Translation team names in the bubble are HTML-escaped.
399 pofile = self.factory.makePOFile('ie')
400 translator_entry = self._makeTranslationTeam(pofile)
401 self._setTeamGuide(pofile, team=translator_entry)
402 translator_entry.translator.displayname = "<blink>Y</blink>"
403
404 view = self._makeView(pofile=pofile)
405 self.assertIn(
406 "&lt;blink&gt;Y&lt;/blink&gt;", view.documentation_link_bubble)
407 self.assertNotIn(
408 translator_entry.translator.displayname,
409 view.documentation_link_bubble)
410
411 def test_documentation_link_bubble_escapes_language_name(self):
412 # Language names in the bubble are HTML-escaped.
413 language = self.factory.makeLanguage(
414 language_code='wtf', name="<blink>Z</blink>")
415 pofile = self.factory.makePOFile('wtf')
416 self._setGroupGuide(pofile)
417 self._setTeamGuide(pofile)
418
419 view = self._makeView(pofile=pofile)
420 self.assertIn(
421 "&lt;blink&gt;Z&lt;/blink&gt;", view.documentation_link_bubble)
422 self.assertNotIn(language.englishname, view.documentation_link_bubble)
423
424
425class TestPOFileBaseViewDocumentation(TestCaseWithFactory,
426 DocumentationScenarioMixin):
427 layer = ZopelessDatabaseLayer
428 view_class = POFileBaseView
429
430
431class TestPOFileTranslateViewDocumentation(TestCaseWithFactory,
432 DocumentationScenarioMixin):
433 layer = ZopelessDatabaseLayer
434 view_class = POFileTranslateView
186435
=== modified file 'lib/lp/translations/interfaces/translationsperson.py'
--- lib/lp/translations/interfaces/translationsperson.py 2010-08-20 20:31:18 +0000
+++ lib/lp/translations/interfaces/translationsperson.py 2010-08-27 10:57:43 +0000
@@ -45,6 +45,9 @@
45 :return: a Storm query result.45 :return: a Storm query result.
46 """46 """
4747
48 def hasTranslated():
49 """Has this user done any translation work?"""
50
48 def getReviewableTranslationFiles(no_older_than=None):51 def getReviewableTranslationFiles(no_older_than=None):
49 """List `POFile`s this person should be able to review.52 """List `POFile`s this person should be able to review.
5053
5154
=== modified file 'lib/lp/translations/model/translationsperson.py'
--- lib/lp/translations/model/translationsperson.py 2010-08-20 20:31:18 +0000
+++ lib/lp/translations/model/translationsperson.py 2010-08-27 10:57:43 +0000
@@ -77,6 +77,10 @@
77 entries = Store.of(self.person).find(POFileTranslator, conditions)77 entries = Store.of(self.person).find(POFileTranslator, conditions)
78 return entries.order_by(Desc(POFileTranslator.date_last_touched))78 return entries.order_by(Desc(POFileTranslator.date_last_touched))
7979
80 def hasTranslated(self):
81 """See `ITranslationsPerson`."""
82 return self.getTranslationHistory().any() is not None
83
80 @property84 @property
81 def translation_history(self):85 def translation_history(self):
82 """See `ITranslationsPerson`."""86 """See `ITranslationsPerson`."""
8387
=== modified file 'lib/lp/translations/templates/pofile-translate.pt'
--- lib/lp/translations/templates/pofile-translate.pt 2010-05-18 18:04:00 +0000
+++ lib/lp/translations/templates/pofile-translate.pt 2010-08-27 10:57:43 +0000
@@ -36,48 +36,7 @@
36 </script>36 </script>
3737
38 <!-- Documentation links -->38 <!-- Documentation links -->
39 <tal:documentation condition="view/translation_group">39 <tal:documentation replace="structure view/documentation_link_bubble" />
40 <div class="important-notice-container"
41 tal:condition="view/has_any_documentation">
42 <div class="important-notice-balloon">
43 <div class="important-notice-buttons">
44 <img class="important-notice-cancel-button" src="/@@/no"
45 alt="Don't show this notice anymore"
46 title="Hide this notice for the duration of this session" />
47 </div>
48 <img src="/@@/info" alt="Information" />
49 <span class="important-notice"
50 tal:condition="view/translation_group/translation_guide_url">
51 Before translating, be sure to go through
52 <a tal:content="string:${view/translation_group/title}
53 instructions"
54 tal:attributes="href
55 view/translation_group/translation_guide_url">
56 translation instructions</a><!--
57 --><tal:has_team
58 condition="view/translation_team"><!--
59 --><tal:has_guidelines
60 tal:condition="view/translation_team/style_guide_url">
61 and <a class="style-guide-url"
62 tal:attributes="
63 href view/translation_team/style_guide_url"
64 tal:content="string:${context/language/englishname}
65 guidelines">Serbian guidelines</a><!--
66 --></tal:has_guidelines><!--
67 --></tal:has_team>.
68 </span>
69 <span class="important-notice"
70 tal:condition="not:view/translation_group/translation_guide_url">
71 Before translating, be sure to go through
72 <a class="style-guide-url"
73 tal:content="string:${view/translation_team/translator/displayname}
74 guidelines"
75 tal:attributes="href view/translation_team/style_guide_url">
76 Serbian guidelines</a>.
77 </span>
78 </div>
79 </div>
80 </tal:documentation>
8140
82 <tal:havepluralforms condition="view/has_plural_form_information">41 <tal:havepluralforms condition="view/has_plural_form_information">
8342
8443
=== added file 'lib/lp/translations/tests/test_translationsperson.py'
--- lib/lp/translations/tests/test_translationsperson.py 1970-01-01 00:00:00 +0000
+++ lib/lp/translations/tests/test_translationsperson.py 2010-08-27 10:57:43 +0000
@@ -0,0 +1,27 @@
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 TranslationsPerson."""
5
6__metaclass__ = type
7
8from canonical.launchpad.webapp.testing import verifyObject
9from canonical.testing import DatabaseFunctionalLayer
10from lp.testing import TestCaseWithFactory
11from lp.translations.interfaces.translationsperson import ITranslationsPerson
12
13
14class TestTranslationsPerson(TestCaseWithFactory):
15 layer = DatabaseFunctionalLayer
16
17 def test_baseline(self):
18 person = ITranslationsPerson(self.factory.makePerson())
19 self.assertTrue(verifyObject(ITranslationsPerson, person))
20
21 def test_hasTranslated(self):
22 person = self.factory.makePerson()
23 translationsperson = ITranslationsPerson(person)
24 self.assertFalse(translationsperson.hasTranslated())
25 self.factory.makeTranslationMessage(
26 translator=person, suggestion=True)
27 self.assertTrue(translationsperson.hasTranslated())