← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~malaria/launchpad/fixes-bug-608631 into lp:launchpad

 

Nicolas Delvaux has proposed merging lp:~malaria/launchpad/fixes-bug-608631 into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  #608631 Visual tag to represent narrow non-breaking spaces
  https://bugs.launchpad.net/bugs/608631


== Summary ==

Bug 608631 describe the fact that there is no easy way to input and to display narrow no-break spaces (U+202F) in Rosetta.
Narrow no-break spaces are mainly used in French before ";:!?»" chars and after "«", but they are also used in some other languages (e.g. for the short form of the Czech dates and others).

Input of NNBSP is a problem for example on MS Windows (users need to mess around in the registry) and on GNU/Linux if your keyboard layout does not provide a shortcut for it.

Display of NNBSP is a problem in some browsers.
All versions of MS Internet Explorer *on Windows XP* display a square instead of the regular character (it works on Vista and Seven, here the bug lays in the OS build-in text renderer). There is also a bug in Qt which means that Qt browsers (Konqueror, Rekonq...) display NNBSP as a 0 width char.
Basically, it displays well on Firefox, GTK based browsers (Epiphany, Midori...) and on IE (on Vista and Seven).

On top of that, it is sometime difficult to tell apart NNBSP from a white space or a no-break space. So, a helper to recognize NNBSP is necessary anyway.

== Proposed fix ==

As discussed with Данило Шеган, it seems that implementing a new tag (as already existing [tab] and [nbsp]) is the best answer to this.

== Pre-implementation notes ==

The obvious tag for NNBSP is [nnbsp], but it's too close to [nbsp].
Basically, NNBSP is just the no-break version of the thin space (U+2009) which HTML shortcut is  . But [nbthinsp] is too long, so I just proposed [nbthin].
So far, no one found something better.

== Implementation details ==

Implementation was quite easy with the existing code for other tags.
There is no really new code, so this should not cause anything to break (that wouldn't have broke without this patch I mean ;-)).

== Test ==

bin/test -cvv -m lp.translations -t browser-helpers.txt

== Demo and Q/A ==

To demo and Q/A this change, do the following:

- Log on as Sample Person (test@xxxxxxxxxxxxx:test)
- Visit any translation page (e.g. https://translations.launchpad.dev/evolution/trunk/+pots/evolution-2.2-test/es/+translate )
- Input something like "Test[nbthin]!" and/or it's escaped version "Test\[nbthin]!" and/or the 'literal' version "Test !"
- Save your translation(s) and see the results (better here with the above example: https://translations.launchpad.dev/evolution/trunk/+pots/evolution-2.2-test/es/+filter?person=name12 )

== Launchpad lint ==

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/translations/browser/browser_helpers.py
  lib/lp/translations/doc/browser-helpers.txt
  lib/lp/translations/interfaces/translations.py


I fixed all warnings in these files (most of them where not related to my code)
-- 
https://code.launchpad.net/~malaria/launchpad/fixes-bug-608631/+merge/37286
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~malaria/launchpad/fixes-bug-608631 into lp:launchpad.
=== modified file 'lib/canonical/launchpad/blocked.html'
--- lib/canonical/launchpad/blocked.html	2010-01-20 09:19:28 +0000
+++ lib/canonical/launchpad/blocked.html	2010-10-01 16:44:57 +0000
@@ -13,12 +13,12 @@
 
         <h1>You have been blocked</h1>
         <p>
-          Due to what we suspect to be inappropriate usage 
+          Due to what we suspect to be inappropriate usage
           of Launchpad, your access has been blocked.
         </p>
         <p>
           If you believe this to be in error, please contact
-          us at feedback@xxxxxxxxxxxxx.
+          us at <a href="/support">Launchpad Support</a>.
         </p>
 
     </div>

=== modified file 'lib/canonical/launchpad/browser/launchpad.py'
--- lib/canonical/launchpad/browser/launchpad.py	2010-09-29 00:49:50 +0000
+++ lib/canonical/launchpad/browser/launchpad.py	2010-10-01 16:44:57 +0000
@@ -483,10 +483,10 @@
 
     @stepto('support')
     def redirect_support(self):
-        """Redirect /support to Answers root site."""
+        """Redirect /support to launchpad Answers site."""
         target_url = canonical_url(
-            getUtility(ILaunchpadRoot), rootsite='answers')
-        return self.redirectSubTree(target_url + 'questions', status=301)
+            getUtility(ILaunchpadCelebrities).launchpad, rootsite='answers')
+        return self.redirectSubTree(target_url, status=301)
 
     @stepto('legal')
     def redirect_legal(self):

=== modified file 'lib/canonical/launchpad/emailtemplates/help.txt'
--- lib/canonical/launchpad/emailtemplates/help.txt	2010-03-12 15:52:45 +0000
+++ lib/canonical/launchpad/emailtemplates/help.txt	2010-10-01 16:44:57 +0000
@@ -270,9 +270,9 @@
 
 https://wiki.launchpad.canonical.com/Bugs/EmailInterface
 
-If you've waited several minutes and still not received either a
-change notification or an error message, please email
-feedback@xxxxxxxxxxxxx
+If you've waited several minutes and still have not received either a
+change notification or an error message, ask a question at
+https://launchpad.net/support
 
 
 Filtering bug mail

=== modified file 'lib/canonical/launchpad/templates/launchpad-gone.pt'
--- lib/canonical/launchpad/templates/launchpad-gone.pt	2009-12-23 16:17:12 +0000
+++ lib/canonical/launchpad/templates/launchpad-gone.pt	2010-10-01 16:44:57 +0000
@@ -12,8 +12,11 @@
       <p>There&#8217;s no page with this address in Launchpad.</p>
 
       <p tal:condition="view/referrer">
-        If this is blocking your work, let us know by sending an message to
-        <a href="mailto:feedback@xxxxxxxxxxxxx";>feedback@xxxxxxxxxxxxx</a>.
+        If this is blocking your work, let us know by asking a question at
+        <a tal:condition="not: view/user|nothing"
+            href="/feedback">Launchpad Support</a>
+        <a tal:condition="view/user|nothing"
+          href="/support">Launchpad Support</a>.
         Otherwise, complain to the maintainers of the page that linked here.
       </p>
 

=== modified file 'lib/canonical/launchpad/templates/launchpad-notfound.pt'
--- lib/canonical/launchpad/templates/launchpad-notfound.pt	2009-07-17 17:59:07 +0000
+++ lib/canonical/launchpad/templates/launchpad-notfound.pt	2010-10-01 16:44:57 +0000
@@ -21,9 +21,11 @@
           Otherwise, complain to the maintainers of the page that linked here.
         </p>
         <p>
-          If this is blocking your work, let us know by sending an message to
-          <a href="mailto:feedback@xxxxxxxxxxxxx";
-             >feedback@xxxxxxxxxxxxx</a>.
+          If this is blocking your work, let us know by asking a question at
+          <a tal:condition="not: view/user|nothing"
+            href="/feedback">Launchpad Support</a>
+          <a tal:condition="view/user|nothing"
+            href="/support">Launchpad Support</a>.
           Include the error <abbr>ID</abbr>
           <code class="oopsid" tal:content="request/oopsid">OOPS-A1234</code>
           in your message.

=== modified file 'lib/lp/answers/stories/question-compatibility-urls.txt'
--- lib/lp/answers/stories/question-compatibility-urls.txt	2009-09-23 14:40:53 +0000
+++ lib/lp/answers/stories/question-compatibility-urls.txt	2010-10-01 16:44:57 +0000
@@ -4,17 +4,6 @@
 As part of that rename many URLs were changed to reflect the new
 terminology. We provide redirect from the old names to the new ones.
 
-
-== Main application ==
-
-    >>> browser.open('http://launchpad.dev/support')
-    >>> print browser.url
-    http://answers.launchpad.dev/questions
-
-    >>> browser.open('http://launchpad.dev/support/+tickets')
-    >>> print browser.url
-    http://answers.launchpad.dev/questions/+questions
-
 == Answer Contact Page ==
 
     >>> user_browser.open('http://launchpad.dev/firefox/+support-contact')

=== modified file 'lib/lp/app/browser/tests/base-layout.txt'
--- lib/lp/app/browser/tests/base-layout.txt	2010-09-03 19:44:29 +0000
+++ lib/lp/app/browser/tests/base-layout.txt	2010-10-01 16:44:57 +0000
@@ -170,7 +170,7 @@
     Read the guide https://help.launchpad.net/
     Canonical&nbsp;Ltd. http://canonical.com/
     Terms of use http://launchpad.dev/legal
-    Contact Launchpad Support https://help.launchpad.net/Feedback
+    Contact Launchpad Support /feedback
     System status http://identi.ca/launchpadstatus
 
 

=== modified file 'lib/lp/app/browser/tests/test_base_layout.py'
--- lib/lp/app/browser/tests/test_base_layout.py	2010-08-20 20:31:18 +0000
+++ lib/lp/app/browser/tests/test_base_layout.py	2010-10-01 16:44:57 +0000
@@ -159,3 +159,21 @@
         self.assertEqual(None, document.find(True, id='watermark'))
         self.assertEqual(None, document.find(True, id='side-portlets'))
         self.assertEqual(None, document.find(True, id='globalsearch'))
+
+    def test_contact_support_logged_in(self):
+        # The support link points to /support when the user is logged in.
+        view = self.makeTemplateView('main_only')
+        view._user = self.user
+        content = BeautifulSoup(view())
+        footer = find_tag_by_id(content, 'footer')
+        link = footer.find('a', text='Contact Launchpad Support').parent
+        self.assertEqual('/support', link['href'])
+
+    def test_contact_support_anonymous(self):
+        # The support link points to /feedback when the user is anonymous.
+        view = self.makeTemplateView('main_only')
+        view._user = None
+        content = BeautifulSoup(view())
+        footer = find_tag_by_id(content, 'footer')
+        link = footer.find('a', text='Contact Launchpad Support').parent
+        self.assertEqual('/feedback', link['href'])

=== modified file 'lib/lp/app/browser/tests/test_launchpadroot.py'
--- lib/lp/app/browser/tests/test_launchpadroot.py	2010-08-20 20:31:18 +0000
+++ lib/lp/app/browser/tests/test_launchpadroot.py	2010-10-01 16:44:57 +0000
@@ -22,6 +22,7 @@
     login_person,
     TestCaseWithFactory,
     )
+from lp.testing.publication import test_traverse
 from lp.testing.views import (
     create_initialized_view,
     create_view,
@@ -81,3 +82,29 @@
         self.failUnless(
             content.find('a', href='+featuredprojects'),
             "Cannot find the +featuredprojects link on the first page")
+
+
+class TestLaunchpadRootNavigation(TestCaseWithFactory):
+    """Test for the LaunchpadRootNavigation."""
+
+    layer = DatabaseFunctionalLayer
+
+    def test_support(self):
+        # The /support link redirects to answers.
+        context, view, request = test_traverse(
+            'http://launchpad.dev/support')
+        view()
+        self.assertEqual(301, request.response.getStatus())
+        self.assertEqual(
+            'http://answers.launchpad.dev/launchpad',
+            request.response.getHeader('location'))
+
+    def test_feedback(self):
+        # The /feedback link redirects to the help site.
+        context, view, request = test_traverse(
+            'http://launchpad.dev/feedback')
+        view()
+        self.assertEqual(301, request.response.getStatus())
+        self.assertEqual(
+            'https://help.launchpad.net/Feedback',
+            request.response.getHeader('location'))

=== modified file 'lib/lp/app/templates/base-layout-macros.pt'
--- lib/lp/app/templates/base-layout-macros.pt	2010-09-22 17:11:24 +0000
+++ lib/lp/app/templates/base-layout-macros.pt	2010-10-01 16:44:57 +0000
@@ -417,8 +417,10 @@
       &nbsp;&bull;&nbsp;
       <a tal:attributes="href string:${rooturl}legal">Terms of use</a>
       &nbsp;&bull;&nbsp;
-      <a href="https://help.launchpad.net/Feedback";
-        >Contact Launchpad Support</a>
+      <a tal:condition="not: view/user|nothing"
+        href="/feedback">Contact Launchpad Support</a>
+      <a tal:condition="view/user|nothing"
+        href="/support">Contact Launchpad Support</a>
       &nbsp;&bull;&nbsp;
       <a href="http://identi.ca/launchpadstatus";
         >System status</a>

=== modified file 'lib/lp/bugs/browser/bugwatch.py'
--- lib/lp/bugs/browser/bugwatch.py	2010-09-24 22:30:48 +0000
+++ lib/lp/bugs/browser/bugwatch.py	2010-10-01 16:44:57 +0000
@@ -29,6 +29,7 @@
 from canonical.launchpad.webapp.menu import structured
 from canonical.widgets.textwidgets import URIWidget
 from lp.bugs.browser.bugtask import get_comments_for_bugtask
+from lp.bugs.interfaces.bugmessage import IBugMessageSet
 from lp.bugs.interfaces.bugwatch import (
     BUG_WATCH_ACTIVITY_SUCCESS_STATUSES,
     IBugWatch,
@@ -131,7 +132,9 @@
 
     def bugWatchIsUnlinked(self, action):
         """Return whether the bug watch is unlinked."""
-        return len(self.context.bugtasks) == 0
+        return (
+            len(self.context.bugtasks) == 0 and
+            self.context.getImportedBugMessages().is_empty())
 
     @action('Delete Bug Watch', name='delete', condition=bugWatchIsUnlinked)
     def delete_action(self, action, data):

=== added file 'lib/lp/bugs/browser/tests/test_bugwatch_views.py'
--- lib/lp/bugs/browser/tests/test_bugwatch_views.py	1970-01-01 00:00:00 +0000
+++ lib/lp/bugs/browser/tests/test_bugwatch_views.py	2010-10-01 16:44:57 +0000
@@ -0,0 +1,56 @@
+# Copyright 2010 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Tests for BugWatch views."""
+
+__metaclass__ = type
+
+from zope.component import getUtility
+
+from canonical.launchpad.interfaces.message import IMessageSet
+from canonical.testing import LaunchpadFunctionalLayer
+
+from lp.testing import login, login_person, TestCaseWithFactory
+from lp.testing.sampledata import ADMIN_EMAIL
+from lp.testing.views import create_initialized_view
+
+
+class TestBugWatchEditView(TestCaseWithFactory):
+
+    layer = LaunchpadFunctionalLayer
+
+    def setUp(self):
+        super(TestBugWatchEditView, self).setUp()
+        self.person = self.factory.makePerson()
+
+        login_person(self.person)
+        self.bug_task = self.factory.makeBug(
+            owner=self.person).default_bugtask
+        self.bug_watch = self.factory.makeBugWatch(
+            bug=self.bug_task.bug)
+
+    def test_cannot_delete_watch_if_linked_to_task(self):
+        # It isn't possible to delete a bug watch that's linked to a bug
+        # task.
+        self.bug_task.bugwatch = self.bug_watch
+        view = create_initialized_view(self.bug_watch, '+edit')
+        self.assertFalse(
+            view.bugWatchIsUnlinked(None),
+            "bugWatchIsUnlinked() returned True though there is a task "
+            "linked to the watch.")
+
+    def test_cannot_delete_watch_if_linked_to_coment(self):
+        # It isn't possible to delete a bug watch that's linked to a bug
+        # comment.
+        message = getUtility(IMessageSet).fromText(
+            "Example message", "With some example content to read.")
+        # We need to log in as an admin here as only admins can link a
+        # watch to a comment.
+        login(ADMIN_EMAIL)
+        bug_message = self.bug_watch.addComment('comment-id', message)
+        login_person(self.person)
+        view = create_initialized_view(self.bug_watch, '+edit')
+        self.assertFalse(
+            view.bugWatchIsUnlinked(None),
+            "bugWatchIsUnlinked() returned True though there is a comment "
+            "linked to the watch.")

=== modified file 'lib/lp/bugs/doc/bugmessage.txt'
--- lib/lp/bugs/doc/bugmessage.txt	2010-05-21 17:01:20 +0000
+++ lib/lp/bugs/doc/bugmessage.txt	2010-10-01 16:44:57 +0000
@@ -109,6 +109,16 @@
     http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=304014
     http://some.bugzilla/show_bug.cgi?id=9876
 
+Note that although the watch was created when the Message was added to
+the bug, the message and the watch are not linked because the message
+was not imported by the bug watch.
+
+    >>> bug_message = bug_one.bug_messages[-1]
+    >>> print bug_message.message == test_message
+    True
+    >>> print bug_message.bugwatch
+    None
+
 CVE watches and bug watches are also created, when a message is imported from
 an external bug tracker.
 

=== modified file 'lib/lp/bugs/model/bugwatch.py'
--- lib/lp/bugs/model/bugwatch.py	2010-08-26 12:12:00 +0000
+++ lib/lp/bugs/model/bugwatch.py	2010-10-01 16:44:57 +0000
@@ -7,6 +7,7 @@
 __all__ = [
     'BugWatch',
     'BugWatchActivity',
+    'BugWatchDeletionError',
     'BugWatchSet',
     ]
 
@@ -114,6 +115,10 @@
                 '%r is not a bug watch or an ID.' % (reference,))
 
 
+class BugWatchDeletionError(Exception):
+    """Raised when someone attempts to delete a linked watch."""
+
+
 class BugWatch(SQLBase):
     """See `IBugWatch`."""
     implements(IBugWatch)
@@ -223,8 +228,16 @@
 
     def destroySelf(self):
         """See `IBugWatch`."""
-        assert len(self.bugtasks) == 0, "Can't delete linked bug watches"
+        if (len(self.bugtasks) > 0 or
+            not self.getImportedBugMessages().is_empty()):
+            raise BugWatchDeletionError(
+                "Can't delete bug watches linked to tasks or comments.")
+        # XXX 2010-09-29 gmb bug=647103
+        #     We flush the store to make sure that errors bubble up and
+        #     are caught by the OOPS machinery.
         SQLBase.destroySelf(self)
+        store = Store.of(self)
+        store.flush()
 
     @property
     def unpushed_comments(self):

=== modified file 'lib/lp/bugs/stories/bugwatches/xx-delete-bugwatch.txt'
--- lib/lp/bugs/stories/bugwatches/xx-delete-bugwatch.txt	2009-09-04 00:37:37 +0000
+++ lib/lp/bugs/stories/bugwatches/xx-delete-bugwatch.txt	2010-10-01 16:44:57 +0000
@@ -1,7 +1,7 @@
 = Delete a Bug Watch =
 
-If a bug watch isn't linked to a bug task, it's possible to delete from
-the bug watch edit page.
+If a bug watch isn't linked to a bug task and no comments have been
+imported for it, it's possible to delete from the bug watch edit page.
 
     >>> user_browser.open('http://launchpad.dev/bugs/1')
     >>> bug_watches = find_portlet(user_browser.contents, 'Remote bug watches')
@@ -32,4 +32,3 @@
     mozilla.org #2000
     mozilla.org #42
     debbugs #304014
-

=== modified file 'lib/lp/bugs/templates/bugtarget-macros-filebug.pt'
--- lib/lp/bugs/templates/bugtarget-macros-filebug.pt	2010-08-24 20:11:37 +0000
+++ lib/lp/bugs/templates/bugtarget-macros-filebug.pt	2010-10-01 16:44:57 +0000
@@ -219,7 +219,7 @@
           </tal:has-bugtracker>
           <tal:XXX condition="nothing">
             XXX: Gavin Panella 2009-09-14 bug=429354:
-            Asking people to use feedback@xxxxxxxxxxxxx is not
+            Asking people to use answers is not
             ideal. We could instead link project owners to the +edit
             page and provide the +contactuser form for everyone else
             to contact the owner.
@@ -228,8 +228,8 @@
             <p class="warning message">
               Launchpad doesn't know what bug tracker <span
               tal:replace="product_or_distro/displayname">Alsa
-              Utils</span> uses. Do you know? <a href="mailto:
-              feedback@xxxxxxxxxxxxx">Tell us about it.</a>
+              Utils</span> uses. Do you know?
+              <a href="/support">Tell us about it.</a>
             </p>
           </tal:not-has-bugtracker>
         </tal:defines>

=== modified file 'lib/lp/bugs/templates/bugwatch-error-help.pt'
--- lib/lp/bugs/templates/bugwatch-error-help.pt	2010-07-15 08:21:40 +0000
+++ lib/lp/bugs/templates/bugwatch-error-help.pt	2010-10-01 16:44:57 +0000
@@ -1,4 +1,5 @@
-<html>
+<html
+  xmlns:tal="http://xml.zope.org/namespaces/tal";>
   <head>
     <title>Bug watch errors</title>
     <link rel="stylesheet" type="text/css"
@@ -23,7 +24,7 @@
     </p>
     <h3>How can I help fix it?</h3>
     <p>
-      <a href="mailto:feedback@xxxxxxxxxxxxx";>Contact us</a> and let us
+      <a href="/support">Contact us</a> and let us
       know about the problem.
     </p>
     <a name="BUG_NOT_FOUND"></a>
@@ -51,8 +52,8 @@
     <p>
       Check that the remote bug tracker
       (<a tal:replace="structure watch/bugtracker/fmt:external-link" />)
-      is on-line. If it is, you should 
-      <a href="mailto:feedback@xxxxxxxxxxxxx";>contact us</a> and let us
+      is on-line. If it is, you should
+      <a href="/support">contact us</a> and let us
       know about the problem.
     </p>
     <a name="INVALID_BUG_ID"></a>
@@ -64,7 +65,7 @@
     </p>
     <h3>How can I help fix it?</h3>
     <p>
-      <a href="mailto:feedback@xxxxxxxxxxxxx";>Contact us</a> and let us
+      <a href="/support">Contact us</a> and let us
       know about the problem.
     </p>
     <a name="TIMEOUT"></a>
@@ -76,14 +77,14 @@
     </p>
     <h3>How can I help fix it?</h3>
     <p>
-      <a href="mailto:feedback@xxxxxxxxxxxxx";>Contact us</a> and let us
+      <a href="/support">Contact us</a> and let us
       know about the problem.
     </p>
     <p>
       Check that the remote bug tracker
       (<a tal:replace="structure watch/bugtracker/fmt:external-link" />)
       is on-line. If it is, you should
-      <a href="mailto:feedback@xxxxxxxxxxxxx";>contact us</a> and let us
+      <a href="/support">contact us</a> and let us
       know about the problem.
     </p>
     <a name="UNPARSABLE_BUG"></a>
@@ -101,7 +102,7 @@
     </p>
     <p>
       If the error has occurred more than once, you should
-      <a href="mailto:feedback@xxxxxxxxxxxxx";>contact us</a> and let us
+      <a href="/support">contact us</a> and let us
       know about the problem.
     </p>
     <a name="UNPARSABLE_BUG_TRACKER"></a>
@@ -113,7 +114,7 @@
     </p>
     <h3>How can I help fix it?</h3>
     <p>
-      <a href="mailto:feedback@xxxxxxxxxxxxx";>Contact us</a> and let us
+      <a href="/support">Contact us</a> and let us
       know about the problem.
     </p>
     <a name="UNSUPPORTED_BUG_TRACKER"></a>
@@ -152,7 +153,7 @@
     </p>
     <h3>How can I help fix it?</h3>
     <p>
-      <a href="mailto:feedback@xxxxxxxxxxxxx";>Contact us</a> and let us
+      <a href="/support">Contact us</a> and let us
       know about the problem.
     </p>
     <a name="COMMENT_PUSH_FAILED"></a>
@@ -165,7 +166,7 @@
     </p>
     <h3>How can I help fix it?</h3>
     <p>
-      <a href="mailto:feedback@xxxxxxxxxxxxx";>Contact us</a> and let us
+      <a href="/support">Contact us</a> and let us
       know about the problem.
     </p>
     <a name="BACKLINK_FAILED"></a>
@@ -178,7 +179,7 @@
     </p>
     <h3>How can I help fix it?</h3>
     <p>
-      <a href="mailto:feedback@xxxxxxxxxxxxx";>Contact us</a> and let us
+      <a href="/support">Contact us</a> and let us
       know about the problem.
     </p>
   </body>

=== modified file 'lib/lp/bugs/tests/test_bugwatch.py'
--- lib/lp/bugs/tests/test_bugwatch.py	2010-08-26 13:10:24 +0000
+++ lib/lp/bugs/tests/test_bugwatch.py	2010-10-01 16:44:57 +0000
@@ -48,7 +48,10 @@
     NoBugTrackerFound,
     UnrecognizedBugTrackerURL,
     )
-from lp.bugs.model.bugwatch import get_bug_watch_ids
+from lp.bugs.model.bugwatch import (
+    BugWatchDeletionError,
+    get_bug_watch_ids,
+    )
 from lp.bugs.scripts.checkwatches.scheduler import MAX_SAMPLE_SIZE
 from lp.registry.interfaces.person import IPersonSet
 from lp.testing import (
@@ -465,6 +468,14 @@
         self.assertRaises(
             AssertionError, list, get_bug_watch_ids(['fred']))
 
+    def test_destroySelf_raise_error_when_linked_to_a_task(self):
+        # It's not possible to delete a bug watch that's linked to a
+        # task. Trying will result in a BugWatchDeletionError.
+        bug_watch = self.factory.makeBugWatch()
+        bug = bug_watch.bug
+        bug.default_bugtask.bugwatch = bug_watch
+        self.assertRaises(BugWatchDeletionError, bug_watch.destroySelf)
+
 
 class TestBugWatchSet(TestCaseWithFactory):
     """Tests for the bugwatch updating system."""

=== modified file 'lib/lp/translations/browser/browser_helpers.py'
--- lib/lp/translations/browser/browser_helpers.py	2010-08-20 20:31:18 +0000
+++ lib/lp/translations/browser/browser_helpers.py	2010-10-01 16:44:57 +0000
@@ -31,7 +31,9 @@
     return helpers.text_replaced(text, {'[tab]': '\t',
                                         r'\[tab]': '[tab]',
                                         '[nbsp]': u'\u00a0',
-                                        r'\[nbsp]': '[nbsp]'})
+                                        r'\[nbsp]': '[nbsp]',
+                                        '[nbthin]': u'\u202f',
+                                        r'\[nbthin]': '[nbthin]'})
 
 
 def expand_rosetta_escapes(unicode_text):
@@ -39,7 +41,10 @@
     escapes = {u'\t': TranslationConstants.TAB_CHAR,
                u'[tab]': TranslationConstants.TAB_CHAR_ESCAPED,
                u'\u00a0': TranslationConstants.NO_BREAK_SPACE_CHAR,
-               u'[nbsp]': TranslationConstants.NO_BREAK_SPACE_CHAR_ESCAPED}
+               u'[nbsp]': TranslationConstants.NO_BREAK_SPACE_CHAR_ESCAPED,
+               u'\u202f': TranslationConstants.NARROW_NO_BREAK_SPACE_CHAR,
+               u'[nbthin]':
+    TranslationConstants.NARROW_NO_BREAK_SPACE_CHAR_ESCAPED}
     return helpers.text_replaced(unicode_text, escapes)
 
 

=== modified file 'lib/lp/translations/doc/browser-helpers.txt'
--- lib/lp/translations/doc/browser-helpers.txt	2009-09-07 07:25:18 +0000
+++ lib/lp/translations/doc/browser-helpers.txt	2010-10-01 16:44:57 +0000
@@ -1,9 +1,11 @@
-= Translation message helper functions =
+Translation message helper functions
+====================================
 
 For rendering translations in the TranslationMessageView a number of
 helper functions exist. The following sections cover them in detail.
 
-== contract_rosetta_escapes ==
+contract_rosetta_escapes
+------------------------
 
     >>> from lp.translations.browser.browser_helpers import (
     ...     contract_rosetta_escapes)
@@ -45,8 +47,20 @@
     >>> contract_rosetta_escapes('foo\\[nbsp]bar')
     'foo[nbsp]bar'
 
-
-== expand_rosetta_escapes ==
+Similarly, string '[nbthin]' gets converted to narrow no-break space
+character.
+
+    >>> contract_rosetta_escapes('foo[nbthin]bar')
+    u'foo\u202fbar'
+
+The string '\[nbthin]' gets converted to a literal '[nbthin]'.
+
+    >>> contract_rosetta_escapes('foo\\[nbthin]bar')
+    'foo[nbthin]bar'
+
+
+expand_rosetta_escapes
+----------------------
 
     >>> from lp.translations.browser.browser_helpers import (
     ...     expand_rosetta_escapes)
@@ -93,8 +107,22 @@
     >>> expand_rosetta_escapes(u'foo[nbsp]bar')
     u'foo<code>\\[nbsp]</code>bar'
 
-
-== parse_cformat_string ==
+Similarly, narrow no-break spaces get converted to a special constant
+TranslationConstants.NARROW_NO_BREAK_SPACE_CHAR which renders as below:
+
+    >>> expand_rosetta_escapes(u'foo\u202fbar')
+    u'foo<code>[nbthin]</code>bar'
+
+Literal occurrences of u'[nbthin]' get escaped to a special constant
+TranslationConstants.NARROW_NO_BREAK_SPACE_CHAR_ESCAPED which renders them
+as below:
+
+    >>> expand_rosetta_escapes(u'foo[nbthin]bar')
+    u'foo<code>\\[nbthin]</code>bar'
+
+
+parse_cformat_string
+--------------------
 
     >>> from lp.translations.browser.browser_helpers import (
     ...     parse_cformat_string)
@@ -112,7 +140,8 @@
     UnrecognisedCFormatString: %
 
 
-== text_to_html ==
+text_to_html
+------------
 
     >>> from lp.translations.browser.browser_helpers import (
     ...     text_to_html)
@@ -178,7 +207,8 @@
     u'foo<img alt="" src="/@@/translation-newline" /><br/>\nbar'
 
 
-== convert_newlines_to_web_form ==
+convert_newlines_to_web_form
+----------------------------
 
     >>> from lp.translations.browser.browser_helpers import (
     ...     convert_newlines_to_web_form)
@@ -194,7 +224,8 @@
     u'foo\r\nbar'
 
 
-== count_lines ==
+count_lines
+-----------
 
     >>> from lp.translations.browser.browser_helpers import count_lines
     >>> count_lines("foo")
@@ -216,8 +247,8 @@
     ...     "1234566789a123456789a")
     2
     >>> count_lines(
-    ...     "123456789abc123456789abc123456789abc123456789abc123456789abc123456\n"
-    ...     "789a123456789a123456789a")
+    ...     "123456789abc123456789abc123456789abc123456789abc123456789abc"
+    ...     "123456\n789a123456789a123456789a")
     3
     >>> count_lines(
     ...     "123456789abc123456789abc123456789abc123456789abc123456789abc"

=== modified file 'lib/lp/translations/interfaces/translations.py'
--- lib/lp/translations/interfaces/translations.py	2010-08-20 20:31:18 +0000
+++ lib/lp/translations/interfaces/translations.py	2010-10-01 16:44:57 +0000
@@ -16,6 +16,7 @@
     'TranslationsBranchImportMode',
     )
 
+
 class TranslationConstants:
     """Set of constants used inside the context of translations."""
 
@@ -31,6 +32,8 @@
     TAB_CHAR_ESCAPED = '<code>' + r'\[tab]' + '</code>'
     NO_BREAK_SPACE_CHAR = '<code>[nbsp]</code>'
     NO_BREAK_SPACE_CHAR_ESCAPED = '<code>' + r'\[nbsp]' + '</code>'
+    NARROW_NO_BREAK_SPACE_CHAR = '<code>[nbthin]</code>'
+    NARROW_NO_BREAK_SPACE_CHAR_ESCAPED = '<code>' + r'\[nbthin]' + '</code>'
 
 
 class TranslationsBranchImportMode(DBEnumeratedType):
@@ -54,5 +57,3 @@
         Import all translation files (templates and translations)
         found in the branch.
         """)
-
-