← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~jtv/launchpad/bug-517700 into lp:launchpad/devel

 

Jeroen T. Vermeulen has proposed merging lp:~jtv/launchpad/bug-517700 into lp:launchpad/devel.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers): code


= 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
-- 
https://code.launchpad.net/~jtv/launchpad/bug-517700/+merge/33888
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~jtv/launchpad/bug-517700 into lp:launchpad/devel.
=== 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:39:58 +0000
@@ -35,7 +35,7 @@
     >>> launchpad_views['small_maps']
     False
 
-Any other value is treated as True because that is default state.
+Any other value is treated as True because that is the default state.
 
     >>> launchpad_views = test_get_launchpad_views(
     ...     'launchpad_views=small_maps=true')
@@ -47,7 +47,7 @@
     >>> launchpad_views['small_maps']
     True
 
-Keys that are note predefined in get_launchpad_views are not accepted.
+Keys that are not predefined in get_launchpad_views are not accepted.
 
     >>> launchpad_views = test_get_launchpad_views(
     ...     'launchpad_views=bad_key=false')

=== 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:39:58 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2010 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Browser code for Translation files."""
@@ -16,11 +16,15 @@
     'POFileView',
     ]
 
-import os.path
+<<<<<<< TREE
+import os.path
+=======
+from cgi import escape
+import os.path
+>>>>>>> MERGE-SOURCE
 import re
 import urllib
 
-from zope.app.form.browser import DropdownWidget
 from zope.component import getUtility
 from zope.publisher.browser import FileUpload
 
@@ -54,15 +58,23 @@
     ITranslationImporter,
     )
 from lp.translations.interfaces.translationimportqueue import (
+<<<<<<< TREE
     ITranslationImportQueue,
     )
 from lp.translations.interfaces.translationsperson import ITranslationsPerson
-
-
-class CustomDropdownWidget(DropdownWidget):
-    def _div(self, cssClass, contents, **kw):
-        """Render the select widget without the div tag."""
-        return contents
+=======
+    ITranslationImportQueue)
+from lp.translations.interfaces.translationsperson import (
+    ITranslationsPerson)
+from canonical.launchpad.interfaces import ILaunchBag
+from canonical.launchpad.webapp import (
+    canonical_url, enabled_with_permission, LaunchpadView,
+    Link, Navigation, NavigationMenu)
+from canonical.launchpad.webapp.batching import BatchNavigator
+from canonical.launchpad.webapp.menu import structured
+
+from canonical.launchpad import _
+>>>>>>> MERGE-SOURCE
 
 
 class POFileNavigation(Navigation):
@@ -143,7 +155,149 @@
     links = ('details', 'translate', 'upload', 'download')
 
 
-class POFileBaseView(LaunchpadView):
+class POFileMetadataViewMixin:
+    """`POFile` metadata that multiple views can use."""
+
+    @cachedproperty
+    def translation_group(self):
+        """Is there a translation group for this translation?
+
+        :return: TranslationGroup or None if not found.
+        """
+        translation_groups = self.context.potemplate.translationgroups
+        if translation_groups is not None and len(translation_groups) > 0:
+            group = translation_groups[0]
+        else:
+            group = None
+        return group
+
+    @cachedproperty
+    def translator_entry(self):
+        """The translator entry or None if none is assigned."""
+        group = self.translation_group
+        if group is not None:
+            return group.query_translator(self.context.language)
+        return None
+
+    @cachedproperty
+    def translator(self):
+        """Who is assigned for translations to this language?"""
+        translator_entry = self.translator_entry
+        if translator_entry is not None:
+            return translator_entry.translator
+        return None
+
+    @cachedproperty
+    def user_is_new_translator(self):
+        """Is this user someone who has done no translation work yet?"""
+        user = getUtility(ILaunchBag).user
+        if user is not None:
+            translationsperson = ITranslationsPerson(user)
+            if not translationsperson.hasTranslated():
+                return True
+
+        return False
+
+    @cachedproperty
+    def translation_group_guide(self):
+        """URL to translation group's translation guide, if any."""
+        group = self.translation_group
+        if group is None:
+            return None
+        else:
+            return group.translation_guide_url
+
+    @cachedproperty
+    def translation_team_guide(self):
+        """URL to translation team's translation guide, if any."""
+        translator = self.translator_entry
+        if translator is None:
+            return None
+        else:
+            return translator.style_guide_url
+
+    @cachedproperty
+    def has_any_documentation(self):
+        """Return whether there is any documentation for this POFile."""
+        return (
+            self.translation_group_guide is not None or
+            self.translation_team_guide is not None or
+            self.user_is_new_translator)
+
+    @property
+    def introduction_link(self):
+        """Link to introductory documentation, if appropriate.
+
+        If no link is appropriate, returns the empty string.
+        """
+        if not self.user_is_new_translator:
+            return ""
+
+        return """
+            New to translating in Launchpad?
+            <a href="/+help/new-to-translating.html" target="help">
+                Read our guide</a>.
+            """
+
+    @property
+    def guide_links(self):
+        """Links to translation group/team guidelines, if available.
+
+        If no guidelines are available, returns the empty string.
+        """
+        group_guide = self.translation_group_guide
+        team_guide = self.translation_team_guide
+        if group_guide is None and team_guide is None:
+            return ""
+
+        links = []
+        if group_guide is not None:
+            links.append("""
+                <a class="style-guide-url" href="%s">%s instructions</a>
+                """ % (group_guide, escape(self.translation_group.title)))
+
+        if team_guide is not None:
+            if group_guide is None:
+                # Use team's full name.
+                name = self.translator.displayname
+            else:
+                # Full team name may get tedious after we just named the
+                # group.  Just use the language name.
+                name = self.context.language.englishname
+            links.append("""
+                <a class="style-guide-url" href="%s"> %s guidelines</a>
+                """ % (team_guide, escape(name)))
+
+        text = ' and '.join(links).rstrip()
+
+        return "Before translating, be sure to go through %s." % text
+
+    @property
+    def documentation_link_bubble(self):
+        """Reference to documentation, if appopriate."""
+        if not self.has_any_documentation:
+            return ""
+
+        return """
+            <div class="important-notice-container">
+                <div class="important-notice-balloon">
+                    <div class="important-notice-buttons">
+                        <img class="important-notice-cancel-button"
+                             src="/@@/no"
+                             alt="Don't show this notice anymore"
+                             title="Hide this notice." />
+                    </div>
+                    <span class="sprite info">
+                    <span class="important-notice">
+                        %s
+                    </span>
+                </div>
+            </div>
+            """ % ' '.join([
+                self.introduction_link, self.guide_links])
+
+
+class POFileBaseView(LaunchpadView, POFileMetadataViewMixin):
     """A basic view for a POFile
 
     This view is different from POFileView as it is the base for a new
@@ -161,7 +315,6 @@
 
         self.batchnav = self._buildBatchNavigator()
 
-
     @cachedproperty
     def contributors(self):
         return tuple(self.context.contributors)
@@ -250,46 +403,6 @@
             return self.context.language.pluralexpression
         return ""
 
-    @cachedproperty
-    def translation_group(self):
-        """Is there a translation group for this translation?
-
-        :return: TranslationGroup or None if not found.
-        """
-        translation_groups = self.context.potemplate.translationgroups
-        if translation_groups is not None and len(translation_groups) > 0:
-            group = translation_groups[0]
-        else:
-            group = None
-        return group
-
-    def _get_translator_entry(self):
-        """The translator entry or None if none is assigned."""
-        group = self.translation_group
-        if group is not None:
-            return group.query_translator(self.context.language)
-        return None
-
-    @cachedproperty
-    def translator(self):
-        """Who is assigned for translations to this language?"""
-        translator_entry = self._get_translator_entry()
-        if translator_entry is not None:
-            return translator_entry.translator
-        return None
-
-    @cachedproperty
-    def has_any_documentation(self):
-        """Return whether there is any documentation for this POFile."""
-        if (self.translation_group is not None and
-            self.translation_group.translation_guide_url is not None):
-            return True
-        translator_entry = self._get_translator_entry()
-        if (translator_entry is not None and
-            translator_entry.style_guide_url is not None):
-            return True
-        return False
-
     def _initializeShowOption(self):
         # Get any value given by the user
         self.show = self.request.form_ng.getOne('show')
@@ -462,6 +575,12 @@
 
 
 class TranslationMessageContainer:
+    """A `TranslationMessage` decorated with usage class.
+
+    The usage class (in-use, hidden" or suggested) is used in CSS to
+    render these messages differently.
+    """
+
     def __init__(self, translation, pofile):
         self.data = translation
 
@@ -478,6 +597,8 @@
 
 
 class FilteredPOTMsgSets:
+    """`POTMsgSet`s and translations shown by the `POFileFilteredView`."""
+
     def __init__(self, translations, pofile):
         potmsgsets = []
         current_potmsgset = None
@@ -494,10 +615,10 @@
                         potmsgsets.append(current_potmsgset)
                     translation.setPOFile(pofile)
                     current_potmsgset = {
-                        'potmsgset' : translation.potmsgset,
-                        'translations' : [TranslationMessageContainer(
-                            translation, pofile)],
-                        'context' : translation
+                        'potmsgset': translation.potmsgset,
+                        'translations': [
+                            TranslationMessageContainer(translation, pofile)],
+                        'context': translation,
                         }
             if current_potmsgset is not None:
                 potmsgsets.append(current_potmsgset)
@@ -523,7 +644,7 @@
         """See `LaunchpadView`."""
         return smartquote('Translations by %s in "%s"') % (
             self._person_name, self.context.title)
-    
+
     def label(self):
         """See `LaunchpadView`."""
         return "Translations by %s" % self._person_name
@@ -663,7 +784,7 @@
         return config.rosetta.translate_pages_max_batch_size
 
 
-class POFileTranslateView(BaseTranslationView):
+class POFileTranslateView(BaseTranslationView, POFileMetadataViewMixin):
     """The View class for a `POFile` or a `DummyPOFile`.
 
     This view is based on `BaseTranslationView` and implements the API
@@ -711,40 +832,6 @@
     # BaseTranslationView API
     #
 
-    @cachedproperty
-    def translation_group(self):
-        """Is there a translation group for this translation?
-
-        :return: TranslationGroup or None if not found.
-        """
-        translation_groups = self.context.potemplate.translationgroups
-        if translation_groups is not None and len(translation_groups) > 0:
-            group = translation_groups[0]
-        else:
-            group = None
-        return group
-
-    @cachedproperty
-    def translation_team(self):
-        """Is there a translation group for this translation."""
-        group = self.translation_group
-        if group is not None:
-            team = group.query_translator(self.context.language)
-        else:
-            team = None
-        return team
-
-    @cachedproperty
-    def has_any_documentation(self):
-        """Return whether there is any documentation for this POFile."""
-        if (self.translation_group is not None and
-            self.translation_group.translation_guide_url is not None):
-            return True
-        if (self.translation_team is not None and
-            self.translation_team.style_guide_url is not None):
-            return True
-        return False
-
     def _buildBatchNavigator(self):
         """See BaseTranslationView._buildBatchNavigator."""
 

=== 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:39:58 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2010 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 __metaclass__ = type
@@ -12,19 +12,30 @@
 import pytz
 
 from canonical.launchpad.webapp.servers import LaunchpadTestRequest
-from canonical.testing import LaunchpadZopelessLayer
+from canonical.testing import ZopelessDatabaseLayer
 from lp.app.errors import UnexpectedFormData
+<<<<<<< TREE
 from lp.testing import TestCaseWithFactory
 from lp.translations.browser.pofile import (
     POFileBaseView,
     POFileTranslateView,
     )
+=======
+from lp.testing import (
+    login,
+    TestCaseWithFactory,
+    )
+from lp.translations.browser.pofile import (
+    POFileBaseView,
+    POFileTranslateView,
+    )
+>>>>>>> MERGE-SOURCE
 
 
 class TestPOFileBaseViewFiltering(TestCaseWithFactory):
     """Test POFileBaseView filtering functions."""
 
-    layer = LaunchpadZopelessLayer
+    layer = ZopelessDatabaseLayer
 
     def gen_now(self):
         now = datetime.now(pytz.UTC)
@@ -161,7 +172,7 @@
 class TestPOFileBaseViewInvalidFiltering(TestCaseWithFactory,
                                          TestInvalidFilteringMixin):
     """Test for POFilleBaseView."""
-    layer = LaunchpadZopelessLayer
+    layer = ZopelessDatabaseLayer
     view_class = POFileBaseView
 
     def setUp(self):
@@ -172,7 +183,7 @@
 class TestPOFileTranslateViewInvalidFiltering(TestCaseWithFactory,
                                               TestInvalidFilteringMixin):
     """Test for POFilleTranslateView."""
-    layer = LaunchpadZopelessLayer
+    layer = ZopelessDatabaseLayer
     view_class = POFileTranslateView
 
     def setUp(self):
@@ -180,6 +191,253 @@
         self.pofile = self.factory.makePOFile('eo')
 
 
-def test_suite():
-    return TestLoader().loadTestsFromName(__name__)
-
+class DocumentationScenarioMixin:
+    """Tests for `POFileBaseView` and `POFileTranslateView`."""
+    # The view class that's being tested.
+    view_class = None
+
+    def _makeLoggedInUser(self):
+        """Create a user, and log in as that user."""
+        email = self.factory.getUniqueString() + '@example.com'
+        user = self.factory.makePerson(email=email)
+        login(email)
+        return user
+
+    def _useNonnewTranslator(self):
+        """Create a user who's done translations, and log in as that user."""
+        user = self._makeLoggedInUser()
+        self.factory.makeSharedTranslationMessage(
+            translator=user, suggestion=True)
+        return user
+
+    def _makeView(self, pofile=None, request=None):
+        """Create a view of type `view_class`.
+
+        :param pofile: An optional `POFile`.  If not given, one will be
+            created.
+        :param request: An optional `LaunchpadTestRequest`.  If not
+            given, one will be created.
+        """
+        if pofile is None:
+            pofile = self.factory.makePOFile('cy')
+        if request is None:
+            request = LaunchpadTestRequest()
+        return self.view_class(pofile, request)
+
+    def _makeTranslationGroup(self, pofile):
+        """Set up a translation group for pofile if it doesn't have one."""
+        product = pofile.potemplate.productseries.product
+        if product.translationgroup is None:
+            product.translationgroup = self.factory.makeTranslationGroup()
+        return product.translationgroup
+
+    def _makeTranslationTeam(self, pofile):
+        """Create a translation team applying to pofile."""
+        language = pofile.language.code
+        group = self._makeTranslationGroup(pofile)
+        return self.factory.makeTranslator(language, group=group)
+
+    def _setGroupGuide(self, pofile):
+        """Set the translation group guide URL for pofile."""
+        guide = "http://%s.example.com/"; % self.factory.getUniqueString()
+        self._makeTranslationGroup(pofile).translation_guide_url = guide
+        return guide
+
+    def _setTeamGuide(self, pofile, team=None):
+        """Set the translation team style guide URL for pofile."""
+        guide = "http://%s.example.com/"; % self.factory.getUniqueString()
+        if team is None:
+            team = self._makeTranslationTeam(pofile)
+        team.style_guide_url = guide
+        return guide
+
+    def _showsIntro(self, bubble_text):
+        """Does bubble_text show the intro for new translators?"""
+        return "New to translating in Launchpad?" in bubble_text
+
+    def _showsGuides(self, bubble_text):
+        """Does bubble_text show translation group/team guidelines?"""
+        return "Before translating" in bubble_text
+
+    def test_user_is_new_translator_anonymous(self):
+        # An anonymous user is not a new translator.
+        self.assertFalse(self._makeView().user_is_new_translator)
+
+    def test_user_is_new_translator_new(self):
+        # A user who's never done any translations is a new translator.
+        self._makeLoggedInUser()
+        self.assertTrue(self._makeView().user_is_new_translator)
+
+    def test_user_is_new_translator_not_new(self):
+        # A user who has done translations is not a new translator.
+        self._useNonnewTranslator()
+        self.assertFalse(self._makeView().user_is_new_translator)
+
+    def test_translation_group_guide_nogroup(self):
+        # If there's no translation group, there is no
+        # translation_group_guide.
+        self.assertIs(None, self._makeView().translation_group_guide)
+
+    def test_translation_group_guide_noguide(self):
+        # The translation group may not have a translation guide.
+        pofile = self.factory.makePOFile('ca')
+        self._makeTranslationGroup(pofile)
+
+        view = self._makeView(pofile=pofile)
+        self.assertIs(None, view.translation_group_guide)
+
+    def test_translation_group_guide(self):
+        # translation_group_guide returns the translation group's style
+        # guide URL if there is one.
+        pofile = self.factory.makePOFile('ce')
+        url = self._setGroupGuide(pofile)
+
+        view = self._makeView(pofile=pofile)
+        self.assertEqual(url, view.translation_group_guide)
+
+    def test_translation_team_guide_nogroup(self):
+        # If there is no translation group, there is no translation team
+        # style guide.
+        self.assertIs(None, self._makeView().translation_team_guide)
+
+    def test_translation_team_guide_noteam(self):
+        # If there is no translation team for this language, there is on
+        # translation team style guide.
+        pofile = self.factory.makePOFile('ch')
+        self._makeTranslationGroup(pofile)
+
+        view = self._makeView(pofile=pofile)
+        self.assertIs(None, view.translation_team_guide)
+
+    def test_translation_team_guide_noguide(self):
+        # A translation team may not have a translation style guide.
+        pofile = self.factory.makePOFile('co')
+        self._makeTranslationTeam(pofile)
+
+        view = self._makeView(pofile=pofile)
+        self.assertIs(None, view.translation_team_guide)
+
+    def test_translation_team_guide(self):
+        # translation_team_guide returns the translation team's
+        # style guide, if there is one.
+        pofile = self.factory.makePOFile('cy')
+        url = self._setTeamGuide(pofile)
+
+        view = self._makeView(pofile=pofile)
+        self.assertEqual(url, view.translation_team_guide)
+
+    def test_documentation_link_bubble_empty(self):
+        # If the user is not a new translator and neither a translation
+        # group nor a team style guide applies, the documentation bubble
+        # is empty.
+        pofile = self.factory.makePOFile('da')
+        self._useNonnewTranslator()
+
+        view = self._makeView(pofile=pofile)
+        self.assertEqual('', view.documentation_link_bubble)
+        self.assertFalse(self._showsIntro(view.documentation_link_bubble))
+        self.assertFalse(self._showsGuides(view.documentation_link_bubble))
+
+    def test_documentation_link_bubble_intro(self):
+        # New users are shown an intro link.
+        self._makeLoggedInUser()
+
+        view = self._makeView()
+        self.assertTrue(self._showsIntro(view.documentation_link_bubble))
+        self.assertFalse(self._showsGuides(view.documentation_link_bubble))
+
+    def test_documentation_link_bubble_group_guide(self):
+        # A translation group's guide shows up in the documentation
+        # bubble.
+        pofile = self.factory.makePOFile('de')
+        self._setGroupGuide(pofile)
+
+        view = self._makeView(pofile=pofile)
+        self.assertFalse(self._showsIntro(view.documentation_link_bubble))
+        self.assertTrue(self._showsGuides(view.documentation_link_bubble))
+
+    def test_documentation_link_bubble_team_guide(self):
+        # A translation team's style guide shows up in the documentation
+        # bubble.
+        pofile = self.factory.makePOFile('de')
+        self._setTeamGuide(pofile)
+
+        view = self._makeView(pofile=pofile)
+        self.assertFalse(self._showsIntro(view.documentation_link_bubble))
+        self.assertTrue(self._showsGuides(view.documentation_link_bubble))
+
+    def test_documentation_link_bubble_both_guides(self):
+        # The documentation bubble can show both a translation group's
+        # guidelines and a translation team's style guide.
+        pofile = self.factory.makePOFile('dv')
+        self._setGroupGuide(pofile)
+        self._setTeamGuide(pofile)
+
+        view = self._makeView(pofile=pofile)
+        self.assertFalse(self._showsIntro(view.documentation_link_bubble))
+        self.assertTrue(self._showsGuides(view.documentation_link_bubble))
+        self.assertIn(" and ", view.documentation_link_bubble)
+
+    def test_documentation_link_bubble_shows_all(self):
+        # So in all, the bubble can show 3 different documentation
+        # links.
+        pofile = self.factory.makePOFile('dz')
+        self._makeLoggedInUser()
+        self._setGroupGuide(pofile)
+        self._setTeamGuide(pofile)
+
+        view = self._makeView(pofile=pofile)
+        self.assertTrue(self._showsIntro(view.documentation_link_bubble))
+        self.assertTrue(self._showsGuides(view.documentation_link_bubble))
+        self.assertIn(" and ", view.documentation_link_bubble)
+
+    def test_documentation_link_bubble_escapes_group_title(self):
+        # Translation group titles in the bubble are HTML-escaped.
+        pofile = self.factory.makePOFile('eo')
+        group = self._makeTranslationGroup(pofile)
+        self._setGroupGuide(pofile)
+        group.title = "<blink>X</blink>"
+
+        view = self._makeView(pofile=pofile)
+        self.assertIn(
+            "&lt;blink&gt;X&lt;/blink&gt;", view.documentation_link_bubble)
+        self.assertNotIn(group.title, view.documentation_link_bubble)
+
+    def test_documentation_link_bubble_escapes_team_name(self):
+        # Translation team names in the bubble are HTML-escaped.
+        pofile = self.factory.makePOFile('ie')
+        translator_entry = self._makeTranslationTeam(pofile)
+        self._setTeamGuide(pofile, team=translator_entry)
+        translator_entry.translator.displayname = "<blink>Y</blink>"
+
+        view = self._makeView(pofile=pofile)
+        self.assertIn(
+            "&lt;blink&gt;Y&lt;/blink&gt;", view.documentation_link_bubble)
+        self.assertNotIn(
+            translator_entry.translator.displayname,
+            view.documentation_link_bubble)
+
+    def test_documentation_link_bubble_escapes_language_name(self):
+        # Language names in the bubble are HTML-escaped.
+        language = self.factory.makeLanguage(
+            language_code='wtf', name="<blink>Z</blink>")
+        pofile = self.factory.makePOFile('wtf')
+        self._setGroupGuide(pofile)
+        self._setTeamGuide(pofile)
+
+        view = self._makeView(pofile=pofile)
+        self.assertIn(
+            "&lt;blink&gt;Z&lt;/blink&gt;", view.documentation_link_bubble)
+        self.assertNotIn(language.englishname, view.documentation_link_bubble)
+
+
+class TestPOFileBaseViewDocumentation(TestCaseWithFactory,
+                                      DocumentationScenarioMixin):
+    layer = ZopelessDatabaseLayer
+    view_class = POFileBaseView
+
+
+class TestPOFileTranslateViewDocumentation(TestCaseWithFactory,
+                                           DocumentationScenarioMixin):
+    layer = ZopelessDatabaseLayer
+    view_class = POFileTranslateView

=== 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:39:58 +0000
@@ -45,6 +45,9 @@
         :return: a Storm query result.
         """
 
+    def hasTranslated():
+        """Has this user done any translation work?"""
+
     def getReviewableTranslationFiles(no_older_than=None):
         """List `POFile`s this person should be able to review.
 

=== 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:39:58 +0000
@@ -77,6 +77,10 @@
         entries = Store.of(self.person).find(POFileTranslator, conditions)
         return entries.order_by(Desc(POFileTranslator.date_last_touched))
 
+    def hasTranslated(self):
+        """See `ITranslationsPerson`."""
+        return self.getTranslationHistory().any() is not None
+
     @property
     def translation_history(self):
         """See `ITranslationsPerson`."""

=== 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:39:58 +0000
@@ -36,48 +36,7 @@
       </script>
 
         <!-- Documentation links -->
-        <tal:documentation condition="view/translation_group">
-          <div class="important-notice-container"
-               tal:condition="view/has_any_documentation">
-            <div class="important-notice-balloon">
-              <div class="important-notice-buttons">
-                <img class="important-notice-cancel-button" src="/@@/no"
-                     alt="Don't show this notice anymore"
-                     title="Hide this notice for the duration of this session" />
-              </div>
-              <img src="/@@/info" alt="Information" />
-              <span class="important-notice"
-                  tal:condition="view/translation_group/translation_guide_url">
-                Before translating, be sure to go through
-                <a tal:content="string:${view/translation_group/title}
-                                instructions"
-                   tal:attributes="href
-                        view/translation_group/translation_guide_url">
-                  translation instructions</a><!--
-             --><tal:has_team
-                   condition="view/translation_team"><!--
-                  --><tal:has_guidelines
-                      tal:condition="view/translation_team/style_guide_url">
-                  and <a class="style-guide-url"
-                         tal:attributes="
-                           href view/translation_team/style_guide_url"
-                         tal:content="string:${context/language/englishname}
-                                      guidelines">Serbian guidelines</a><!--
-               --></tal:has_guidelines><!--
-             --></tal:has_team>.
-              </span>
-              <span class="important-notice"
-                  tal:condition="not:view/translation_group/translation_guide_url">
-                Before translating, be sure to go through
-                <a class="style-guide-url"
-                   tal:content="string:${view/translation_team/translator/displayname}
-                                guidelines"
-                   tal:attributes="href view/translation_team/style_guide_url">
-                  Serbian guidelines</a>.
-              </span>
-            </div>
-          </div>
-        </tal:documentation>
+        <tal:documentation replace="structure view/documentation_link_bubble" />
 
       <tal:havepluralforms condition="view/has_plural_form_information">
 

=== 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:39:58 +0000
@@ -0,0 +1,27 @@
+# Copyright 2010 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Unit tests for TranslationsPerson."""
+
+__metaclass__ = type
+
+from canonical.launchpad.webapp.testing import verifyObject
+from canonical.testing import DatabaseFunctionalLayer
+from lp.testing import TestCaseWithFactory
+from lp.translations.interfaces.translationsperson import ITranslationsPerson
+
+
+class TestTranslationsPerson(TestCaseWithFactory):
+    layer = DatabaseFunctionalLayer
+
+    def test_baseline(self):
+        person = ITranslationsPerson(self.factory.makePerson())
+        self.assertTrue(verifyObject(ITranslationsPerson, person))
+
+    def test_hasTranslated(self):
+        person = self.factory.makePerson()
+        translationsperson = ITranslationsPerson(person)
+        self.assertFalse(translationsperson.hasTranslated())
+        self.factory.makeTranslationMessage(
+            translator=person, suggestion=True)
+        self.assertTrue(translationsperson.hasTranslated())