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
1=== modified file 'lib/canonical/launchpad/doc/launchpad-views-cookie.txt'
2--- lib/canonical/launchpad/doc/launchpad-views-cookie.txt 2009-03-06 19:09:45 +0000
3+++ lib/canonical/launchpad/doc/launchpad-views-cookie.txt 2010-08-27 10:57:43 +0000
4@@ -35,7 +35,7 @@
5 >>> launchpad_views['small_maps']
6 False
7
8-Any other value is treated as True because that is default state.
9+Any other value is treated as True because that is the default state.
10
11 >>> launchpad_views = test_get_launchpad_views(
12 ... 'launchpad_views=small_maps=true')
13@@ -47,7 +47,7 @@
14 >>> launchpad_views['small_maps']
15 True
16
17-Keys that are note predefined in get_launchpad_views are not accepted.
18+Keys that are not predefined in get_launchpad_views are not accepted.
19
20 >>> launchpad_views = test_get_launchpad_views(
21 ... 'launchpad_views=bad_key=false')
22
23=== modified file 'lib/lp/translations/browser/pofile.py'
24--- lib/lp/translations/browser/pofile.py 2010-08-20 20:31:18 +0000
25+++ lib/lp/translations/browser/pofile.py 2010-08-27 10:57:43 +0000
26@@ -1,4 +1,4 @@
27-# Copyright 2009 Canonical Ltd. This software is licensed under the
28+# Copyright 2009-2010 Canonical Ltd. This software is licensed under the
29 # GNU Affero General Public License version 3 (see the file LICENSE).
30
31 """Browser code for Translation files."""
32@@ -16,17 +16,18 @@
33 'POFileView',
34 ]
35
36+from cgi import escape
37 import os.path
38 import re
39 import urllib
40
41-from zope.app.form.browser import DropdownWidget
42 from zope.component import getUtility
43 from zope.publisher.browser import FileUpload
44
45 from canonical.cachedproperty import cachedproperty
46 from canonical.config import config
47 from canonical.launchpad import _
48+from canonical.launchpad.interfaces import ILaunchBag
49 from canonical.launchpad.webapp import (
50 canonical_url,
51 enabled_with_permission,
52@@ -59,12 +60,6 @@
53 from lp.translations.interfaces.translationsperson import ITranslationsPerson
54
55
56-class CustomDropdownWidget(DropdownWidget):
57- def _div(self, cssClass, contents, **kw):
58- """Render the select widget without the div tag."""
59- return contents
60-
61-
62 class POFileNavigation(Navigation):
63
64 usedfor = IPOFile
65@@ -143,7 +138,149 @@
66 links = ('details', 'translate', 'upload', 'download')
67
68
69-class POFileBaseView(LaunchpadView):
70+class POFileMetadataViewMixin:
71+ """`POFile` metadata that multiple views can use."""
72+
73+ @cachedproperty
74+ def translation_group(self):
75+ """Is there a translation group for this translation?
76+
77+ :return: TranslationGroup or None if not found.
78+ """
79+ translation_groups = self.context.potemplate.translationgroups
80+ if translation_groups is not None and len(translation_groups) > 0:
81+ group = translation_groups[0]
82+ else:
83+ group = None
84+ return group
85+
86+ @cachedproperty
87+ def translator_entry(self):
88+ """The translator entry or None if none is assigned."""
89+ group = self.translation_group
90+ if group is not None:
91+ return group.query_translator(self.context.language)
92+ return None
93+
94+ @cachedproperty
95+ def translator(self):
96+ """Who is assigned for translations to this language?"""
97+ translator_entry = self.translator_entry
98+ if translator_entry is not None:
99+ return translator_entry.translator
100+ return None
101+
102+ @cachedproperty
103+ def user_is_new_translator(self):
104+ """Is this user someone who has done no translation work yet?"""
105+ user = getUtility(ILaunchBag).user
106+ if user is not None:
107+ translationsperson = ITranslationsPerson(user)
108+ if not translationsperson.hasTranslated():
109+ return True
110+
111+ return False
112+
113+ @cachedproperty
114+ def translation_group_guide(self):
115+ """URL to translation group's translation guide, if any."""
116+ group = self.translation_group
117+ if group is None:
118+ return None
119+ else:
120+ return group.translation_guide_url
121+
122+ @cachedproperty
123+ def translation_team_guide(self):
124+ """URL to translation team's translation guide, if any."""
125+ translator = self.translator_entry
126+ if translator is None:
127+ return None
128+ else:
129+ return translator.style_guide_url
130+
131+ @cachedproperty
132+ def has_any_documentation(self):
133+ """Return whether there is any documentation for this POFile."""
134+ return (
135+ self.translation_group_guide is not None or
136+ self.translation_team_guide is not None or
137+ self.user_is_new_translator)
138+
139+ @property
140+ def introduction_link(self):
141+ """Link to introductory documentation, if appropriate.
142+
143+ If no link is appropriate, returns the empty string.
144+ """
145+ if not self.user_is_new_translator:
146+ return ""
147+
148+ return """
149+ New to translating in Launchpad?
150+ <a href="/+help/new-to-translating.html" target="help">
151+ Read our guide</a>.
152+ """
153+
154+ @property
155+ def guide_links(self):
156+ """Links to translation group/team guidelines, if available.
157+
158+ If no guidelines are available, returns the empty string.
159+ """
160+ group_guide = self.translation_group_guide
161+ team_guide = self.translation_team_guide
162+ if group_guide is None and team_guide is None:
163+ return ""
164+
165+ links = []
166+ if group_guide is not None:
167+ links.append("""
168+ <a class="style-guide-url" href="%s">%s instructions</a>
169+ """ % (group_guide, escape(self.translation_group.title)))
170+
171+ if team_guide is not None:
172+ if group_guide is None:
173+ # Use team's full name.
174+ name = self.translator.displayname
175+ else:
176+ # Full team name may get tedious after we just named the
177+ # group. Just use the language name.
178+ name = self.context.language.englishname
179+ links.append("""
180+ <a class="style-guide-url" href="%s"> %s guidelines</a>
181+ """ % (team_guide, escape(name)))
182+
183+ text = ' and '.join(links).rstrip()
184+
185+ return "Before translating, be sure to go through %s." % text
186+
187+ @property
188+ def documentation_link_bubble(self):
189+ """Reference to documentation, if appopriate."""
190+ if not self.has_any_documentation:
191+ return ""
192+
193+ return """
194+ <div class="important-notice-container">
195+ <div class="important-notice-balloon">
196+ <div class="important-notice-buttons">
197+ <img class="important-notice-cancel-button"
198+ src="/@@/no"
199+ alt="Don't show this notice anymore"
200+ title="Hide this notice." />
201+ </div>
202+ <span class="sprite info">
203+ <span class="important-notice">
204+ %s
205+ </span>
206+ </div>
207+ </div>
208+ """ % ' '.join([
209+ self.introduction_link, self.guide_links])
210+
211+
212+class POFileBaseView(LaunchpadView, POFileMetadataViewMixin):
213 """A basic view for a POFile
214
215 This view is different from POFileView as it is the base for a new
216@@ -161,7 +298,6 @@
217
218 self.batchnav = self._buildBatchNavigator()
219
220-
221 @cachedproperty
222 def contributors(self):
223 return tuple(self.context.contributors)
224@@ -250,46 +386,6 @@
225 return self.context.language.pluralexpression
226 return ""
227
228- @cachedproperty
229- def translation_group(self):
230- """Is there a translation group for this translation?
231-
232- :return: TranslationGroup or None if not found.
233- """
234- translation_groups = self.context.potemplate.translationgroups
235- if translation_groups is not None and len(translation_groups) > 0:
236- group = translation_groups[0]
237- else:
238- group = None
239- return group
240-
241- def _get_translator_entry(self):
242- """The translator entry or None if none is assigned."""
243- group = self.translation_group
244- if group is not None:
245- return group.query_translator(self.context.language)
246- return None
247-
248- @cachedproperty
249- def translator(self):
250- """Who is assigned for translations to this language?"""
251- translator_entry = self._get_translator_entry()
252- if translator_entry is not None:
253- return translator_entry.translator
254- return None
255-
256- @cachedproperty
257- def has_any_documentation(self):
258- """Return whether there is any documentation for this POFile."""
259- if (self.translation_group is not None and
260- self.translation_group.translation_guide_url is not None):
261- return True
262- translator_entry = self._get_translator_entry()
263- if (translator_entry is not None and
264- translator_entry.style_guide_url is not None):
265- return True
266- return False
267-
268 def _initializeShowOption(self):
269 # Get any value given by the user
270 self.show = self.request.form_ng.getOne('show')
271@@ -462,6 +558,12 @@
272
273
274 class TranslationMessageContainer:
275+ """A `TranslationMessage` decorated with usage class.
276+
277+ The usage class (in-use, hidden" or suggested) is used in CSS to
278+ render these messages differently.
279+ """
280+
281 def __init__(self, translation, pofile):
282 self.data = translation
283
284@@ -478,6 +580,8 @@
285
286
287 class FilteredPOTMsgSets:
288+ """`POTMsgSet`s and translations shown by the `POFileFilteredView`."""
289+
290 def __init__(self, translations, pofile):
291 potmsgsets = []
292 current_potmsgset = None
293@@ -494,10 +598,10 @@
294 potmsgsets.append(current_potmsgset)
295 translation.setPOFile(pofile)
296 current_potmsgset = {
297- 'potmsgset' : translation.potmsgset,
298- 'translations' : [TranslationMessageContainer(
299- translation, pofile)],
300- 'context' : translation
301+ 'potmsgset': translation.potmsgset,
302+ 'translations': [
303+ TranslationMessageContainer(translation, pofile)],
304+ 'context': translation,
305 }
306 if current_potmsgset is not None:
307 potmsgsets.append(current_potmsgset)
308@@ -523,7 +627,7 @@
309 """See `LaunchpadView`."""
310 return smartquote('Translations by %s in "%s"') % (
311 self._person_name, self.context.title)
312-
313+
314 def label(self):
315 """See `LaunchpadView`."""
316 return "Translations by %s" % self._person_name
317@@ -663,7 +767,7 @@
318 return config.rosetta.translate_pages_max_batch_size
319
320
321-class POFileTranslateView(BaseTranslationView):
322+class POFileTranslateView(BaseTranslationView, POFileMetadataViewMixin):
323 """The View class for a `POFile` or a `DummyPOFile`.
324
325 This view is based on `BaseTranslationView` and implements the API
326@@ -711,40 +815,6 @@
327 # BaseTranslationView API
328 #
329
330- @cachedproperty
331- def translation_group(self):
332- """Is there a translation group for this translation?
333-
334- :return: TranslationGroup or None if not found.
335- """
336- translation_groups = self.context.potemplate.translationgroups
337- if translation_groups is not None and len(translation_groups) > 0:
338- group = translation_groups[0]
339- else:
340- group = None
341- return group
342-
343- @cachedproperty
344- def translation_team(self):
345- """Is there a translation group for this translation."""
346- group = self.translation_group
347- if group is not None:
348- team = group.query_translator(self.context.language)
349- else:
350- team = None
351- return team
352-
353- @cachedproperty
354- def has_any_documentation(self):
355- """Return whether there is any documentation for this POFile."""
356- if (self.translation_group is not None and
357- self.translation_group.translation_guide_url is not None):
358- return True
359- if (self.translation_team is not None and
360- self.translation_team.style_guide_url is not None):
361- return True
362- return False
363-
364 def _buildBatchNavigator(self):
365 """See BaseTranslationView._buildBatchNavigator."""
366
367
368=== modified file 'lib/lp/translations/browser/tests/test_pofile_view.py'
369--- lib/lp/translations/browser/tests/test_pofile_view.py 2010-08-20 20:31:18 +0000
370+++ lib/lp/translations/browser/tests/test_pofile_view.py 2010-08-27 10:57:43 +0000
371@@ -1,4 +1,4 @@
372-# Copyright 2009 Canonical Ltd. This software is licensed under the
373+# Copyright 2009-2010 Canonical Ltd. This software is licensed under the
374 # GNU Affero General Public License version 3 (see the file LICENSE).
375
376 __metaclass__ = type
377@@ -7,14 +7,16 @@
378 datetime,
379 timedelta,
380 )
381-from unittest import TestLoader
382
383 import pytz
384
385 from canonical.launchpad.webapp.servers import LaunchpadTestRequest
386-from canonical.testing import LaunchpadZopelessLayer
387+from canonical.testing import ZopelessDatabaseLayer
388 from lp.app.errors import UnexpectedFormData
389-from lp.testing import TestCaseWithFactory
390+from lp.testing import (
391+ login,
392+ TestCaseWithFactory,
393+ )
394 from lp.translations.browser.pofile import (
395 POFileBaseView,
396 POFileTranslateView,
397@@ -24,7 +26,7 @@
398 class TestPOFileBaseViewFiltering(TestCaseWithFactory):
399 """Test POFileBaseView filtering functions."""
400
401- layer = LaunchpadZopelessLayer
402+ layer = ZopelessDatabaseLayer
403
404 def gen_now(self):
405 now = datetime.now(pytz.UTC)
406@@ -161,7 +163,7 @@
407 class TestPOFileBaseViewInvalidFiltering(TestCaseWithFactory,
408 TestInvalidFilteringMixin):
409 """Test for POFilleBaseView."""
410- layer = LaunchpadZopelessLayer
411+ layer = ZopelessDatabaseLayer
412 view_class = POFileBaseView
413
414 def setUp(self):
415@@ -172,7 +174,7 @@
416 class TestPOFileTranslateViewInvalidFiltering(TestCaseWithFactory,
417 TestInvalidFilteringMixin):
418 """Test for POFilleTranslateView."""
419- layer = LaunchpadZopelessLayer
420+ layer = ZopelessDatabaseLayer
421 view_class = POFileTranslateView
422
423 def setUp(self):
424@@ -180,6 +182,253 @@
425 self.pofile = self.factory.makePOFile('eo')
426
427
428-def test_suite():
429- return TestLoader().loadTestsFromName(__name__)
430-
431+class DocumentationScenarioMixin:
432+ """Tests for `POFileBaseView` and `POFileTranslateView`."""
433+ # The view class that's being tested.
434+ view_class = None
435+
436+ def _makeLoggedInUser(self):
437+ """Create a user, and log in as that user."""
438+ email = self.factory.getUniqueString() + '@example.com'
439+ user = self.factory.makePerson(email=email)
440+ login(email)
441+ return user
442+
443+ def _useNonnewTranslator(self):
444+ """Create a user who's done translations, and log in as that user."""
445+ user = self._makeLoggedInUser()
446+ self.factory.makeSharedTranslationMessage(
447+ translator=user, suggestion=True)
448+ return user
449+
450+ def _makeView(self, pofile=None, request=None):
451+ """Create a view of type `view_class`.
452+
453+ :param pofile: An optional `POFile`. If not given, one will be
454+ created.
455+ :param request: An optional `LaunchpadTestRequest`. If not
456+ given, one will be created.
457+ """
458+ if pofile is None:
459+ pofile = self.factory.makePOFile('cy')
460+ if request is None:
461+ request = LaunchpadTestRequest()
462+ return self.view_class(pofile, request)
463+
464+ def _makeTranslationGroup(self, pofile):
465+ """Set up a translation group for pofile if it doesn't have one."""
466+ product = pofile.potemplate.productseries.product
467+ if product.translationgroup is None:
468+ product.translationgroup = self.factory.makeTranslationGroup()
469+ return product.translationgroup
470+
471+ def _makeTranslationTeam(self, pofile):
472+ """Create a translation team applying to pofile."""
473+ language = pofile.language.code
474+ group = self._makeTranslationGroup(pofile)
475+ return self.factory.makeTranslator(language, group=group)
476+
477+ def _setGroupGuide(self, pofile):
478+ """Set the translation group guide URL for pofile."""
479+ guide = "http://%s.example.com/" % self.factory.getUniqueString()
480+ self._makeTranslationGroup(pofile).translation_guide_url = guide
481+ return guide
482+
483+ def _setTeamGuide(self, pofile, team=None):
484+ """Set the translation team style guide URL for pofile."""
485+ guide = "http://%s.example.com/" % self.factory.getUniqueString()
486+ if team is None:
487+ team = self._makeTranslationTeam(pofile)
488+ team.style_guide_url = guide
489+ return guide
490+
491+ def _showsIntro(self, bubble_text):
492+ """Does bubble_text show the intro for new translators?"""
493+ return "New to translating in Launchpad?" in bubble_text
494+
495+ def _showsGuides(self, bubble_text):
496+ """Does bubble_text show translation group/team guidelines?"""
497+ return "Before translating" in bubble_text
498+
499+ def test_user_is_new_translator_anonymous(self):
500+ # An anonymous user is not a new translator.
501+ self.assertFalse(self._makeView().user_is_new_translator)
502+
503+ def test_user_is_new_translator_new(self):
504+ # A user who's never done any translations is a new translator.
505+ self._makeLoggedInUser()
506+ self.assertTrue(self._makeView().user_is_new_translator)
507+
508+ def test_user_is_new_translator_not_new(self):
509+ # A user who has done translations is not a new translator.
510+ self._useNonnewTranslator()
511+ self.assertFalse(self._makeView().user_is_new_translator)
512+
513+ def test_translation_group_guide_nogroup(self):
514+ # If there's no translation group, there is no
515+ # translation_group_guide.
516+ self.assertIs(None, self._makeView().translation_group_guide)
517+
518+ def test_translation_group_guide_noguide(self):
519+ # The translation group may not have a translation guide.
520+ pofile = self.factory.makePOFile('ca')
521+ self._makeTranslationGroup(pofile)
522+
523+ view = self._makeView(pofile=pofile)
524+ self.assertIs(None, view.translation_group_guide)
525+
526+ def test_translation_group_guide(self):
527+ # translation_group_guide returns the translation group's style
528+ # guide URL if there is one.
529+ pofile = self.factory.makePOFile('ce')
530+ url = self._setGroupGuide(pofile)
531+
532+ view = self._makeView(pofile=pofile)
533+ self.assertEqual(url, view.translation_group_guide)
534+
535+ def test_translation_team_guide_nogroup(self):
536+ # If there is no translation group, there is no translation team
537+ # style guide.
538+ self.assertIs(None, self._makeView().translation_team_guide)
539+
540+ def test_translation_team_guide_noteam(self):
541+ # If there is no translation team for this language, there is on
542+ # translation team style guide.
543+ pofile = self.factory.makePOFile('ch')
544+ self._makeTranslationGroup(pofile)
545+
546+ view = self._makeView(pofile=pofile)
547+ self.assertIs(None, view.translation_team_guide)
548+
549+ def test_translation_team_guide_noguide(self):
550+ # A translation team may not have a translation style guide.
551+ pofile = self.factory.makePOFile('co')
552+ self._makeTranslationTeam(pofile)
553+
554+ view = self._makeView(pofile=pofile)
555+ self.assertIs(None, view.translation_team_guide)
556+
557+ def test_translation_team_guide(self):
558+ # translation_team_guide returns the translation team's
559+ # style guide, if there is one.
560+ pofile = self.factory.makePOFile('cy')
561+ url = self._setTeamGuide(pofile)
562+
563+ view = self._makeView(pofile=pofile)
564+ self.assertEqual(url, view.translation_team_guide)
565+
566+ def test_documentation_link_bubble_empty(self):
567+ # If the user is not a new translator and neither a translation
568+ # group nor a team style guide applies, the documentation bubble
569+ # is empty.
570+ pofile = self.factory.makePOFile('da')
571+ self._useNonnewTranslator()
572+
573+ view = self._makeView(pofile=pofile)
574+ self.assertEqual('', view.documentation_link_bubble)
575+ self.assertFalse(self._showsIntro(view.documentation_link_bubble))
576+ self.assertFalse(self._showsGuides(view.documentation_link_bubble))
577+
578+ def test_documentation_link_bubble_intro(self):
579+ # New users are shown an intro link.
580+ self._makeLoggedInUser()
581+
582+ view = self._makeView()
583+ self.assertTrue(self._showsIntro(view.documentation_link_bubble))
584+ self.assertFalse(self._showsGuides(view.documentation_link_bubble))
585+
586+ def test_documentation_link_bubble_group_guide(self):
587+ # A translation group's guide shows up in the documentation
588+ # bubble.
589+ pofile = self.factory.makePOFile('de')
590+ self._setGroupGuide(pofile)
591+
592+ view = self._makeView(pofile=pofile)
593+ self.assertFalse(self._showsIntro(view.documentation_link_bubble))
594+ self.assertTrue(self._showsGuides(view.documentation_link_bubble))
595+
596+ def test_documentation_link_bubble_team_guide(self):
597+ # A translation team's style guide shows up in the documentation
598+ # bubble.
599+ pofile = self.factory.makePOFile('de')
600+ self._setTeamGuide(pofile)
601+
602+ view = self._makeView(pofile=pofile)
603+ self.assertFalse(self._showsIntro(view.documentation_link_bubble))
604+ self.assertTrue(self._showsGuides(view.documentation_link_bubble))
605+
606+ def test_documentation_link_bubble_both_guides(self):
607+ # The documentation bubble can show both a translation group's
608+ # guidelines and a translation team's style guide.
609+ pofile = self.factory.makePOFile('dv')
610+ self._setGroupGuide(pofile)
611+ self._setTeamGuide(pofile)
612+
613+ view = self._makeView(pofile=pofile)
614+ self.assertFalse(self._showsIntro(view.documentation_link_bubble))
615+ self.assertTrue(self._showsGuides(view.documentation_link_bubble))
616+ self.assertIn(" and ", view.documentation_link_bubble)
617+
618+ def test_documentation_link_bubble_shows_all(self):
619+ # So in all, the bubble can show 3 different documentation
620+ # links.
621+ pofile = self.factory.makePOFile('dz')
622+ self._makeLoggedInUser()
623+ self._setGroupGuide(pofile)
624+ self._setTeamGuide(pofile)
625+
626+ view = self._makeView(pofile=pofile)
627+ self.assertTrue(self._showsIntro(view.documentation_link_bubble))
628+ self.assertTrue(self._showsGuides(view.documentation_link_bubble))
629+ self.assertIn(" and ", view.documentation_link_bubble)
630+
631+ def test_documentation_link_bubble_escapes_group_title(self):
632+ # Translation group titles in the bubble are HTML-escaped.
633+ pofile = self.factory.makePOFile('eo')
634+ group = self._makeTranslationGroup(pofile)
635+ self._setGroupGuide(pofile)
636+ group.title = "<blink>X</blink>"
637+
638+ view = self._makeView(pofile=pofile)
639+ self.assertIn(
640+ "&lt;blink&gt;X&lt;/blink&gt;", view.documentation_link_bubble)
641+ self.assertNotIn(group.title, view.documentation_link_bubble)
642+
643+ def test_documentation_link_bubble_escapes_team_name(self):
644+ # Translation team names in the bubble are HTML-escaped.
645+ pofile = self.factory.makePOFile('ie')
646+ translator_entry = self._makeTranslationTeam(pofile)
647+ self._setTeamGuide(pofile, team=translator_entry)
648+ translator_entry.translator.displayname = "<blink>Y</blink>"
649+
650+ view = self._makeView(pofile=pofile)
651+ self.assertIn(
652+ "&lt;blink&gt;Y&lt;/blink&gt;", view.documentation_link_bubble)
653+ self.assertNotIn(
654+ translator_entry.translator.displayname,
655+ view.documentation_link_bubble)
656+
657+ def test_documentation_link_bubble_escapes_language_name(self):
658+ # Language names in the bubble are HTML-escaped.
659+ language = self.factory.makeLanguage(
660+ language_code='wtf', name="<blink>Z</blink>")
661+ pofile = self.factory.makePOFile('wtf')
662+ self._setGroupGuide(pofile)
663+ self._setTeamGuide(pofile)
664+
665+ view = self._makeView(pofile=pofile)
666+ self.assertIn(
667+ "&lt;blink&gt;Z&lt;/blink&gt;", view.documentation_link_bubble)
668+ self.assertNotIn(language.englishname, view.documentation_link_bubble)
669+
670+
671+class TestPOFileBaseViewDocumentation(TestCaseWithFactory,
672+ DocumentationScenarioMixin):
673+ layer = ZopelessDatabaseLayer
674+ view_class = POFileBaseView
675+
676+
677+class TestPOFileTranslateViewDocumentation(TestCaseWithFactory,
678+ DocumentationScenarioMixin):
679+ layer = ZopelessDatabaseLayer
680+ view_class = POFileTranslateView
681
682=== modified file 'lib/lp/translations/interfaces/translationsperson.py'
683--- lib/lp/translations/interfaces/translationsperson.py 2010-08-20 20:31:18 +0000
684+++ lib/lp/translations/interfaces/translationsperson.py 2010-08-27 10:57:43 +0000
685@@ -45,6 +45,9 @@
686 :return: a Storm query result.
687 """
688
689+ def hasTranslated():
690+ """Has this user done any translation work?"""
691+
692 def getReviewableTranslationFiles(no_older_than=None):
693 """List `POFile`s this person should be able to review.
694
695
696=== modified file 'lib/lp/translations/model/translationsperson.py'
697--- lib/lp/translations/model/translationsperson.py 2010-08-20 20:31:18 +0000
698+++ lib/lp/translations/model/translationsperson.py 2010-08-27 10:57:43 +0000
699@@ -77,6 +77,10 @@
700 entries = Store.of(self.person).find(POFileTranslator, conditions)
701 return entries.order_by(Desc(POFileTranslator.date_last_touched))
702
703+ def hasTranslated(self):
704+ """See `ITranslationsPerson`."""
705+ return self.getTranslationHistory().any() is not None
706+
707 @property
708 def translation_history(self):
709 """See `ITranslationsPerson`."""
710
711=== modified file 'lib/lp/translations/templates/pofile-translate.pt'
712--- lib/lp/translations/templates/pofile-translate.pt 2010-05-18 18:04:00 +0000
713+++ lib/lp/translations/templates/pofile-translate.pt 2010-08-27 10:57:43 +0000
714@@ -36,48 +36,7 @@
715 </script>
716
717 <!-- Documentation links -->
718- <tal:documentation condition="view/translation_group">
719- <div class="important-notice-container"
720- tal:condition="view/has_any_documentation">
721- <div class="important-notice-balloon">
722- <div class="important-notice-buttons">
723- <img class="important-notice-cancel-button" src="/@@/no"
724- alt="Don't show this notice anymore"
725- title="Hide this notice for the duration of this session" />
726- </div>
727- <img src="/@@/info" alt="Information" />
728- <span class="important-notice"
729- tal:condition="view/translation_group/translation_guide_url">
730- Before translating, be sure to go through
731- <a tal:content="string:${view/translation_group/title}
732- instructions"
733- tal:attributes="href
734- view/translation_group/translation_guide_url">
735- translation instructions</a><!--
736- --><tal:has_team
737- condition="view/translation_team"><!--
738- --><tal:has_guidelines
739- tal:condition="view/translation_team/style_guide_url">
740- and <a class="style-guide-url"
741- tal:attributes="
742- href view/translation_team/style_guide_url"
743- tal:content="string:${context/language/englishname}
744- guidelines">Serbian guidelines</a><!--
745- --></tal:has_guidelines><!--
746- --></tal:has_team>.
747- </span>
748- <span class="important-notice"
749- tal:condition="not:view/translation_group/translation_guide_url">
750- Before translating, be sure to go through
751- <a class="style-guide-url"
752- tal:content="string:${view/translation_team/translator/displayname}
753- guidelines"
754- tal:attributes="href view/translation_team/style_guide_url">
755- Serbian guidelines</a>.
756- </span>
757- </div>
758- </div>
759- </tal:documentation>
760+ <tal:documentation replace="structure view/documentation_link_bubble" />
761
762 <tal:havepluralforms condition="view/has_plural_form_information">
763
764
765=== added file 'lib/lp/translations/tests/test_translationsperson.py'
766--- lib/lp/translations/tests/test_translationsperson.py 1970-01-01 00:00:00 +0000
767+++ lib/lp/translations/tests/test_translationsperson.py 2010-08-27 10:57:43 +0000
768@@ -0,0 +1,27 @@
769+# Copyright 2010 Canonical Ltd. This software is licensed under the
770+# GNU Affero General Public License version 3 (see the file LICENSE).
771+
772+"""Unit tests for TranslationsPerson."""
773+
774+__metaclass__ = type
775+
776+from canonical.launchpad.webapp.testing import verifyObject
777+from canonical.testing import DatabaseFunctionalLayer
778+from lp.testing import TestCaseWithFactory
779+from lp.translations.interfaces.translationsperson import ITranslationsPerson
780+
781+
782+class TestTranslationsPerson(TestCaseWithFactory):
783+ layer = DatabaseFunctionalLayer
784+
785+ def test_baseline(self):
786+ person = ITranslationsPerson(self.factory.makePerson())
787+ self.assertTrue(verifyObject(ITranslationsPerson, person))
788+
789+ def test_hasTranslated(self):
790+ person = self.factory.makePerson()
791+ translationsperson = ITranslationsPerson(person)
792+ self.assertFalse(translationsperson.hasTranslated())
793+ self.factory.makeTranslationMessage(
794+ translator=person, suggestion=True)
795+ self.assertTrue(translationsperson.hasTranslated())