← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~adeuring/launchpad/bug-594247-unittests-for-searchtasks-4 into lp:launchpad/devel

 

Abel Deuring has proposed merging lp:~adeuring/launchpad/bug-594247-unittests-for-searchtasks-4 into lp:launchpad/devel.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)


This branch adds more unit tests for BugTaskSet.search() and for
BugTaskSet.searchBugIds(), leaving only a few parameters of
BugTaskSearchParams not covered.

Aside for these additional tests, I added a new method
assertSearchFinds() (suggested by Gavin in a previous review
of these tests), which makes reading the tests slightly less
boring and a bit more readable. Working on this change, I
noticed that one tests missed an assert...

Working on tests to find bugs being created or modified after a
given time, I noticed that it was possible to pass the parameters
created_since and modified_since to the constructor of
BugTaskSearchparams, but that the object properties created_since
and modified_since were always set to None. This is now fixed.

One test needed access to a product which is not the main
target of the current test; an already existing test modifies
the bug task of this "other target"
(changeStatusOfBugTaskForOtherProduct()). I moved the code to find
this "other bugtask" into a separate method
(findBugtaskForOtherProduct()). The implementation is less obsucre
ini comparison with the old implementation to find the "other
bugtask".

test: ./bin/test -vvt test_bugtask_search

no lint.

-- 
https://code.launchpad.net/~adeuring/launchpad/bug-594247-unittests-for-searchtasks-4/+merge/39989
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~adeuring/launchpad/bug-594247-unittests-for-searchtasks-4 into lp:launchpad/devel.
=== modified file 'lib/canonical/launchpad/browser/launchpad.py'
=== modified file 'lib/canonical/launchpad/icing/style-3-0.css.in'
=== modified file 'lib/lp/app/browser/linkchecker.py'
--- lib/lp/app/browser/linkchecker.py	2010-11-03 08:15:38 +0000
+++ lib/lp/app/browser/linkchecker.py	2010-11-03 16:36:06 +0000
@@ -1,3 +1,4 @@
+<<<<<<< TREE
 # Copyright 2009 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
@@ -75,3 +76,87 @@
                     NotFoundError):
                 invalid_links.append(link)
         return invalid_links
+=======
+# Copyright 2009 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+# pylint: disable-msg=E0211,E0213
+
+__metaclass__ = type
+__all__ = [
+    'LinkCheckerAPI',
+    ]
+
+import simplejson
+from zope.component import getUtility
+
+from lp.app.errors import NotFoundError
+from lp.code.errors import (
+    CannotHaveLinkedBranch,
+    InvalidNamespace,
+    NoLinkedBranch,
+    NoSuchBranch,
+    )
+from lp.code.interfaces.branchlookup import IBranchLookup
+from lp.registry.interfaces.product import InvalidProductName
+
+
+class LinkCheckerAPI:
+    """Validates Launchpad shortcut links.
+
+    This class provides the endpoint of an Ajax call to .../+check-links.
+    When invoked with a collection of links harvested from a page, it will
+    check the validity of each one and send a response containing those that
+    are invalid. Javascript on the page will set the style of invalid links to
+    something appropriate.
+
+    This initial implementation supports processing links like the following:
+        /+branch/foo/bar
+
+    The implementation can easily be extended to handle other forms by
+    providing a method to handle the link type extracted from the json
+    request.
+    """
+
+    def __init__(self, context, request):
+        # We currently only use the request.
+        # self.context = context
+        self.request = request
+
+        # Each link type has it's own validation method.
+        self.link_checkers = dict(
+            branch_links=self.check_branch_links,
+        )
+
+    def __call__(self):
+        result = {}
+        links_to_check_data = self.request.get('link_hrefs')
+        links_to_check = simplejson.loads(links_to_check_data)
+
+        for link_type in links_to_check:
+            links = links_to_check[link_type]
+            invalid_links = self.link_checkers[link_type](links)
+            result['invalid_'+link_type] = invalid_links
+
+        self.request.response.setHeader('Content-type', 'application/json')
+        return simplejson.dumps(result)
+
+    def check_branch_links(self, links):
+        """Check links of the form /+branch/foo/bar"""
+        invalid_links = {}
+        branch_lookup = getUtility(IBranchLookup)
+        for link in links:
+            path = link.strip('/')[len('+branch/'):]
+            try:
+                branch_lookup.getByLPPath(path)
+            except (CannotHaveLinkedBranch, InvalidNamespace,
+                    InvalidProductName, NoLinkedBranch, NoSuchBranch,
+                    NotFoundError) as e:
+                invalid_links[link] = self._error_message(e)
+        return invalid_links
+
+    def _error_message(self, ex):
+        if hasattr(ex, 'display_message'):
+            return ex.display_message
+        return str(ex)
+>>>>>>> MERGE-SOURCE

=== modified file 'lib/lp/app/javascript/lp-links.js'
--- lib/lp/app/javascript/lp-links.js	2010-11-03 08:15:38 +0000
+++ lib/lp/app/javascript/lp-links.js	2010-11-03 16:36:06 +0000
@@ -1,3 +1,4 @@
+<<<<<<< TREE
 /**
  * Launchpad utilities for manipulating links.
  *
@@ -103,3 +104,105 @@
     "base", "node", "io", "dom", "json"
     ]});
 
+=======
+/**
+ * Launchpad utilities for manipulating links.
+ *
+ * @module app
+ * @submodule links
+ */
+
+YUI.add('lp.app.links', function(Y) {
+
+    function harvest_links(links_holder, link_class, link_type) {
+        // Get any links of the specified link_class and store them as the
+        // specified link_type in the specified links_holder
+        var link_info = Y.Array.unique(
+            Y.all('.' + link_class).getAttribute('href'));
+        if( link_info.length > 0 ) {
+            links_holder[link_type] = link_info;
+        }
+    }
+
+    function process_invalid_links(link_info, link_class, link_type) {
+        // We have a collection of invalid links possibly containing links of
+        // type link_type, so we need to remove the existing link_class,
+        // replace it with an invalid-link class, and set the link title.
+        var invalid_links = link_info['invalid_'+link_type];
+
+        if( Y.Object.size(invalid_links) == 0 )
+            return;
+
+        Y.all('.'+link_class).each(function(link) {
+            var href = link.getAttribute('href');
+            if( !(href in invalid_links) )
+                return;
+            var invalid_link_msg = invalid_links[href];
+            link.removeClass(link_class);
+            link.addClass('invalid-link');
+            link.setAttribute('title', invalid_link_msg);
+            link.on('click', function(e) {
+                e.halt();
+                alert(invalid_link_msg);
+            });
+        });
+    }
+
+    var links = Y.namespace('lp.app.links');
+
+    links.check_valid_lp_links = function() {
+        // Grabs any lp: style links on the page and checks that they are
+        // valid. Invalid ones have their class changed to "invalid-link".
+        // ATM, we only handle +branch links.
+
+        var links_to_check = {}
+
+        // We get all the links with defined css classes.
+        // At the moment, we just handle branch links, but in future...
+        harvest_links(links_to_check, 'branch-short-link', 'branch_links');
+
+        // Do we have anything to do?
+        if( Y.Object.size(links_to_check) == 0 ) {
+            return;
+        }
+
+        // Get the final json to send
+        var json_link_info = Y.JSON.stringify(links_to_check);
+        var qs = '';
+        qs = LP.client.append_qs(qs, 'link_hrefs', json_link_info);
+
+        var config = {
+            on: {
+                failure: function(id, response, args) {
+                    // If we have firebug installed, log the error.
+                    if( console != undefined ) {
+                        console.log("Link Check Error: " + args + ': '
+                                + response.status + ' - ' +
+                                response.statusText + ' - '
+                                + response.responseXML);
+                    }
+                },
+                success: function(id, response) {
+                    var link_info = Y.JSON.parse(response.responseText)
+                    // ATM, we just handle branch links, but in future...
+                    process_invalid_links(link_info, 'branch-short-link',
+                            'branch_links');
+                }
+            }
+        }
+        var uri = '+check-links';
+        var on = Y.merge(config.on);
+        var client = this;
+        var y_config = { method: "POST",
+                         headers: {'Accept': 'application/json'},
+                         on: on,
+                         'arguments': [client, uri],
+                         data: qs};
+        Y.io(uri, y_config);
+    };
+
+}, "0.1", {"requires": [
+    "base", "node", "io", "dom", "json", "collection"
+    ]});
+
+>>>>>>> MERGE-SOURCE

=== modified file 'lib/lp/app/templates/base-layout-macros.pt'
--- lib/lp/app/templates/base-layout-macros.pt	2010-11-03 08:15:38 +0000
+++ lib/lp/app/templates/base-layout-macros.pt	2010-11-03 16:36:06 +0000
@@ -148,6 +148,8 @@
             tal:attributes="src string:${yui}/overlay/overlay.js"></script>
     <script type="text/javascript"
             tal:attributes="src string:${yui}/node-menunav/node-menunav.js"></script>
+     <script type="text/javascript"
+            tal:attributes="src string:${yui}/collection/collection.js"></script>
 
     <script type="text/javascript"
             tal:attributes="src string:${lazr_js}/lazr/lazr.js"></script>

=== modified file 'lib/lp/blueprints/interfaces/specification.py'
=== modified file 'lib/lp/blueprints/model/specification.py'
=== modified file 'lib/lp/bugs/browser/tests/test_bugsubscription_views.py'
--- lib/lp/bugs/browser/tests/test_bugsubscription_views.py	2010-11-02 12:00:57 +0000
+++ lib/lp/bugs/browser/tests/test_bugsubscription_views.py	2010-11-03 16:36:06 +0000
@@ -1,3 +1,4 @@
+<<<<<<< TREE
 # Copyright 2010 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
@@ -91,3 +92,99 @@
                     harness.getFieldError('bug_notification_level'),
                     "The view should treat BugNotificationLevel.NOTHING "
                     "as an invalid value.")
+=======
+# Copyright 2010 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Tests for BugSubscription views."""
+
+__metaclass__ = type
+
+from storm.store import Store
+
+from canonical.launchpad.ftests import LaunchpadFormHarness
+from canonical.testing.layers import LaunchpadFunctionalLayer
+
+from lp.bugs.browser.bugsubscription import BugSubscriptionSubscribeSelfView
+from lp.bugs.model.bugsubscription import BugSubscription
+from lp.registry.enum import BugNotificationLevel
+from lp.testing import (
+    feature_flags,
+    person_logged_in,
+    set_feature_flag,
+    TestCaseWithFactory,
+    )
+
+
+class BugSubscriptionAdvancedFeaturesTestCase(TestCaseWithFactory):
+
+    layer = LaunchpadFunctionalLayer
+
+    def _getBugSubscriptionForUserAndBug(self, user, bug):
+        """Return the BugSubscription for a given user, bug combination."""
+        store = Store.of(bug)
+        return store.find(
+            BugSubscription,
+            BugSubscription.person == user,
+            BugSubscription.bug == bug).one()
+
+    def test_subscribe_uses_bug_notification_level(self):
+        # When a user subscribes to a bug using the advanced features on
+        # the Bug +subscribe page, the bug notification level they
+        # choose is taken into account.
+        bug = self.factory.makeBug()
+        # We unsubscribe the bug's owner because if we don't there will
+        # be two COMMENTS-level subscribers.
+        with person_logged_in(bug.owner):
+            bug.unsubscribe(bug.owner, bug.owner)
+
+        # We don't display BugNotificationLevel.NOTHING as an option.
+        # This is tested below.
+        with feature_flags():
+            set_feature_flag(u'malone.advanced-subscriptions.enabled', u'on')
+            displayed_levels = [
+                level for level in BugNotificationLevel.items
+                if level != BugNotificationLevel.NOTHING]
+            for level in displayed_levels:
+                person = self.factory.makePerson()
+                with person_logged_in(person):
+                    harness = LaunchpadFormHarness(
+                        bug.default_bugtask, BugSubscriptionSubscribeSelfView)
+                    form_data = {
+                        'field.subscription': person.name,
+                        'field.bug_notification_level': level.name,
+                        }
+                    harness.submit('continue', form_data)
+
+        subscription = self._getBugSubscriptionForUserAndBug(
+            person, bug)
+        self.assertEqual(
+            level, subscription.bug_notification_level,
+            "Bug notification level of subscription should be %s, is "
+            "actually %s." % (
+                level.name, subscription.bug_notification_level.name))
+
+        # XXX 2010-11-01 gmb bug=668334:
+        #     This should be in its own test but at the moment the above
+        #     bug causes it to fail because of feature flag
+        #     inconsistencies. It should be moved to its own test when
+        #     the above bug is resolved.
+        # BugNotificationLevel.NOTHING isn't considered valid when
+        # someone is trying to subscribe.
+        with feature_flags():
+            with person_logged_in(person):
+                level = BugNotificationLevel.NOTHING
+                harness = LaunchpadFormHarness(
+                    bug.default_bugtask, BugSubscriptionSubscribeSelfView)
+                form_data = {
+                    'field.subscription': person.name,
+                    'field.bug_notification_level': level.name,
+                    }
+                harness.submit('continue', form_data)
+                self.assertTrue(harness.hasErrors())
+                self.assertEqual(
+                    'Invalid value',
+                    harness.getFieldError('bug_notification_level'),
+                    "The view should treat BugNotificationLevel.NOTHING as an "
+                    "invalid value.")
+>>>>>>> MERGE-SOURCE

=== modified file 'lib/lp/bugs/interfaces/bugtask.py'
--- lib/lp/bugs/interfaces/bugtask.py	2010-11-03 14:31:33 +0000
+++ lib/lp/bugs/interfaces/bugtask.py	2010-11-03 16:36:06 +0000
@@ -1188,8 +1188,8 @@
         self.hardware_is_linked_to_bug = hardware_is_linked_to_bug
         self.linked_branches = linked_branches
         self.structural_subscriber = structural_subscriber
-        self.modified_since = None
-        self.created_since = None
+        self.modified_since = modified_since
+        self.created_since = created_since
 
     def setProduct(self, product):
         """Set the upstream context on which to filter the search."""

=== modified file 'lib/lp/bugs/tests/test_bugtask_search.py'
--- lib/lp/bugs/tests/test_bugtask_search.py	2010-10-29 13:00:57 +0000
+++ lib/lp/bugs/tests/test_bugtask_search.py	2010-11-03 16:36:06 +0000
@@ -25,6 +25,7 @@
 
 from lp.bugs.interfaces.bugattachment import BugAttachmentType
 from lp.bugs.interfaces.bugtask import (
+    BugBranchSearch,
     BugTaskImportance,
     BugTaskSearchParams,
     BugTaskStatus,
@@ -53,29 +54,30 @@
         super(SearchTestBase, self).setUp()
         self.bugtask_set = getUtility(IBugTaskSet)
 
+    def assertSearchFinds(self, params, expected_bugtasks):
+        # Run a search for the given search parameters and check if
+        # the result matches the expected bugtasks.
+        search_result = self.runSearch(params)
+        expected = self.resultValuesForBugtasks(expected_bugtasks)
+        self.assertEqual(expected, search_result)
+
     def test_search_all_bugtasks_for_target(self):
         # BugTaskSet.search() returns all bug tasks for a given bug
         # target, if only the bug target is passed as a search parameter.
         params = self.getBugTaskSearchParams(user=None)
-        search_result = self.runSearch(params)
-        expected = self.resultValuesForBugtasks(self.bugtasks)
-        self.assertEqual(expected, search_result)
+        self.assertSearchFinds(params, self.bugtasks)
 
     def test_private_bug_in_search_result(self):
         # Private bugs are not included in search results for anonymous users.
         with person_logged_in(self.owner):
             self.bugtasks[-1].bug.setPrivate(True, self.owner)
         params = self.getBugTaskSearchParams(user=None)
-        search_result = self.runSearch(params)
-        expected = self.resultValuesForBugtasks(self.bugtasks)[:-1]
-        self.assertEqual(expected, search_result)
+        self.assertSearchFinds(params, self.bugtasks[:-1])
 
         # Private bugs are not included in search results for ordinary users.
         user = self.factory.makePerson()
         params = self.getBugTaskSearchParams(user=user)
-        search_result = self.runSearch(params)
-        expected = self.resultValuesForBugtasks(self.bugtasks)[:-1]
-        self.assertEqual(expected, search_result)
+        self.assertSearchFinds(params, self.bugtasks[:-1])
 
         # If the user is subscribed to the bug, it is included in the
         # search result.
@@ -83,16 +85,12 @@
         with person_logged_in(self.owner):
             self.bugtasks[-1].bug.subscribe(user, self.owner)
         params = self.getBugTaskSearchParams(user=user)
-        search_result = self.runSearch(params)
-        expected = self.resultValuesForBugtasks(self.bugtasks)
-        self.assertEqual(expected, search_result)
+        self.assertSearchFinds(params, self.bugtasks)
 
         # Private bugs are included in search results for admins.
         admin = getUtility(IPersonSet).getByEmail('foo.bar@xxxxxxxxxxxxx')
         params = self.getBugTaskSearchParams(user=admin)
-        search_result = self.runSearch(params)
-        expected = self.resultValuesForBugtasks(self.bugtasks)
-        self.assertEqual(expected, search_result)
+        self.assertSearchFinds(params, self.bugtasks)
 
     def test_search_by_bug_reporter(self):
         # Search results can be limited to bugs filed by a given person.
@@ -100,9 +98,7 @@
         reporter = bugtask.bug.owner
         params = self.getBugTaskSearchParams(
             user=None, bug_reporter=reporter)
-        search_result = self.runSearch(params)
-        expected = self.resultValuesForBugtasks([bugtask])
-        self.assertEqual(expected, search_result)
+        self.assertSearchFinds(params, [bugtask])
 
     def test_search_by_bug_commenter(self):
         # Search results can be limited to bugs having a comment from a
@@ -118,9 +114,7 @@
             expected.bug.newMessage(owner=commenter, content='a comment')
         params = self.getBugTaskSearchParams(
             user=None, bug_commenter=commenter)
-        search_result = self.runSearch(params)
-        expected = self.resultValuesForBugtasks([expected])
-        self.assertEqual(expected, search_result)
+        self.assertSearchFinds(params, [expected])
 
     def test_search_by_person_affected_by_bug(self):
         # Search results can be limited to bugs which affect a given person.
@@ -130,9 +124,7 @@
             expected.bug.markUserAffected(affected_user)
         params = self.getBugTaskSearchParams(
             user=None, affected_user=affected_user)
-        search_result = self.runSearch(params)
-        expected = self.resultValuesForBugtasks([expected])
-        self.assertEqual(expected, search_result)
+        self.assertSearchFinds(params, [expected])
 
     def test_search_by_bugtask_assignee(self):
         # Search results can be limited to bugtask assigned to a given
@@ -142,9 +134,7 @@
         with person_logged_in(assignee):
             expected.transitionToAssignee(assignee)
         params = self.getBugTaskSearchParams(user=None, assignee=assignee)
-        search_result = self.runSearch(params)
-        expected = self.resultValuesForBugtasks([expected])
-        self.assertEqual(expected, search_result)
+        self.assertSearchFinds(params, [expected])
 
     def test_search_by_bug_subscriber(self):
         # Search results can be limited to bugs to which a given person
@@ -154,9 +144,69 @@
         with person_logged_in(subscriber):
             expected.bug.subscribe(subscriber, subscribed_by=subscriber)
         params = self.getBugTaskSearchParams(user=None, subscriber=subscriber)
-        search_result = self.runSearch(params)
-        expected = self.resultValuesForBugtasks([expected])
-        self.assertEqual(expected, search_result)
+        self.assertSearchFinds(params, [expected])
+
+    def subscribeToTarget(self, subscriber):
+        # Subscribe the given person to the search target.
+        with person_logged_in(subscriber):
+            self.searchtarget.addSubscription(
+                subscriber, subscribed_by=subscriber)
+
+    def _findBugtaskForOtherProduct(self, bugtask, main_product):
+        # Return the bugtask for the product that not related to the
+        # main bug target.
+        #
+        # The default bugtasks of this test suite are created by
+        # ObjectFactory.makeBugTask() as follows:
+        # - a new bug is created having a new product as the target.
+        # - another bugtask is created for self.searchtarget (or,
+        #   when self.searchtarget is a milestone, for the product
+        #   of the milestone)
+        # This method returns the bug task for the product that is not
+        # related to the main bug target.
+        bug = bugtask.bug
+        for other_task in bug.bugtasks:
+            other_target = other_task.target
+            if (IProduct.providedBy(other_target)
+                and other_target != main_product):
+                return other_task
+        self.fail(
+            'No bug task found for a product that is not the target of '
+            'the main test bugtask.')
+
+    def findBugtaskForOtherProduct(self, bugtask):
+        # Return the bugtask for the product that not related to the
+        # main bug target.
+        #
+        # This method must ober overridden for product related tests.
+        return self._findBugtaskForOtherProduct(bugtask, None)
+
+    def test_search_by_structural_subscriber(self):
+        # Search results can be limited to bugs with a bug target to which
+        # a given person has a structural subscription.
+        subscriber = self.factory.makePerson()
+        # If the given person is not subscribed, no bugtasks are returned.
+        params = self.getBugTaskSearchParams(
+            user=None, structural_subscriber=subscriber)
+        self.assertSearchFinds(params, [])
+        # When the person is subscribed, all bugtasks are returned.
+        self.subscribeToTarget(subscriber)
+        params = self.getBugTaskSearchParams(
+            user=None, structural_subscriber=subscriber)
+        self.assertSearchFinds(params, self.bugtasks)
+
+        # Searching for a structural subscriber does not return a bugtask,
+        # if the person is subscribed to another target than the main
+        # bug target.
+        other_subscriber = self.factory.makePerson()
+        other_bugtask = self.findBugtaskForOtherProduct(self.bugtasks[0])
+        other_target = other_bugtask.target
+        with person_logged_in(other_subscriber):
+            other_target.addSubscription(
+                other_subscriber, subscribed_by=other_subscriber)
+        params = self.getBugTaskSearchParams(
+            user=None, structural_subscriber=other_subscriber)
+        self.assertSearchFinds(params, [])
 
     def test_search_by_bug_attachment(self):
         # Search results can be limited to bugs having attachments of
@@ -171,23 +221,270 @@
         # We can search for bugs with non-patch attachments...
         params = self.getBugTaskSearchParams(
             user=None, attachmenttype=BugAttachmentType.UNSPECIFIED)
-        search_result = self.runSearch(params)
-        expected = self.resultValuesForBugtasks(self.bugtasks[:1])
-        self.assertEqual(expected, search_result)
+        self.assertSearchFinds(params, self.bugtasks[:1])
         # ... for bugs with patches...
         params = self.getBugTaskSearchParams(
             user=None, attachmenttype=BugAttachmentType.PATCH)
-        search_result = self.runSearch(params)
-        expected = self.resultValuesForBugtasks(self.bugtasks[1:2])
-        self.assertEqual(expected, search_result)
+        self.assertSearchFinds(params, self.bugtasks[1:2])
         # and for bugs with patches or attachments
         params = self.getBugTaskSearchParams(
             user=None, attachmenttype=any(
                 BugAttachmentType.PATCH,
                 BugAttachmentType.UNSPECIFIED))
-        search_result = self.runSearch(params)
-        expected = self.resultValuesForBugtasks(self.bugtasks[:2])
-        self.assertEqual(expected, search_result)
+        self.assertSearchFinds(params, self.bugtasks[:2])
+
+    def setUpFullTextSearchTests(self):
+        # Set text fields indexed by Bug.fti, BugTask.fti or
+        # MessageChunk.fti to values we can search for.
+        for bugtask, number in zip(self.bugtasks, ('one', 'two', 'three')):
+            commenter = self.bugtasks[0].bug.owner
+            with person_logged_in(commenter):
+                bugtask.statusexplanation = 'status explanation %s' % number
+                bugtask.bug.title = 'bug title %s' % number
+                bugtask.bug.newMessage(
+                    owner=commenter, content='comment %s' % number)
+
+    def test_fulltext_search(self):
+        # Full text searches find text indexed by Bug.fti...
+        self.setUpFullTextSearchTests()
+        params = self.getBugTaskSearchParams(
+            user=None, searchtext='one title')
+        self.assertSearchFinds(params, self.bugtasks[:1])
+        # ... by BugTask.fti ...
+        params = self.getBugTaskSearchParams(
+            user=None, searchtext='two explanation')
+        self.assertSearchFinds(params, self.bugtasks[1:2])
+        # ...and by MessageChunk.fti
+        params = self.getBugTaskSearchParams(
+            user=None, searchtext='three comment')
+        self.assertSearchFinds(params, self.bugtasks[2:3])
+
+    def test_fast_fulltext_search(self):
+        # Fast full text searches find text indexed by Bug.fti...
+        self.setUpFullTextSearchTests()
+        params = self.getBugTaskSearchParams(
+            user=None, fast_searchtext='one title')
+        self.assertSearchFinds(params, self.bugtasks[:1])
+        # ... but not text indexed by BugTask.fti ...
+        params = self.getBugTaskSearchParams(
+            user=None, fast_searchtext='two explanation')
+        self.assertSearchFinds(params, [])
+        # ..or by MessageChunk.fti
+        params = self.getBugTaskSearchParams(
+            user=None, fast_searchtext='three comment')
+        self.assertSearchFinds(params, [])
+
+    def test_has_no_upstream_bugtask(self):
+        # Search results can be limited to bugtasks of bugs that do
+        # not have a related upstream task.
+        #
+        # All bugs created in makeBugTasks() have at least one
+        # bug task for a product: The default bug task created
+        # by lp.testing.factory.Factory.makeBug() if neither a
+        # product nor a distribution is specified. For distribution
+        # related tests we need another bug which does not have
+        # an upstream (aka product) bug task, otherwise the set of
+        # bugtasks returned for a search for has_no_upstream_bugtask
+        # would always be empty.
+        if (IDistribution.providedBy(self.searchtarget) or
+            IDistroSeries.providedBy(self.searchtarget) or
+            ISourcePackage.providedBy(self.searchtarget) or
+            IDistributionSourcePackage.providedBy(self.searchtarget)):
+            if IDistribution.providedBy(self.searchtarget):
+                bug = self.factory.makeBug(distribution=self.searchtarget)
+                expected = [bug.default_bugtask]
+            else:
+                bug = self.factory.makeBug(
+                    distribution=self.searchtarget.distribution)
+                bugtask = self.factory.makeBugTask(
+                    bug=bug, target=self.searchtarget)
+                expected = [bugtask]
+        else:
+            # Bugs without distribution related bugtasks have always at
+            # least one product related bugtask, hence a
+            # has_no_upstream_bugtask search will always return an
+            # empty result set.
+            expected = []
+        params = self.getBugTaskSearchParams(
+            user=None, has_no_upstream_bugtask=True)
+        self.assertSearchFinds(params, expected)
+
+    def changeStatusOfBugTaskForOtherProduct(self, bugtask, new_status):
+        # Change the status of another bugtask of the same bug to the
+        # given status.
+        other_task = self.findBugtaskForOtherProduct(bugtask)
+        with person_logged_in(other_task.target.owner):
+            other_task.transitionToStatus(new_status, other_task.target.owner)
+
+    def test_upstream_status(self):
+        # Search results can be filtered by the status of an upstream
+        # bug task.
+        #
+        # The bug task status of the default test data has only bug tasks
+        # with status NEW for the "other" product, hence all bug tasks
+        # will be returned in a search for bugs that are open upstream.
+        params = self.getBugTaskSearchParams(user=None, open_upstream=True)
+        self.assertSearchFinds(params, self.bugtasks)
+        # A search for tasks resolved upstream does not yield any bugtask.
+        params = self.getBugTaskSearchParams(
+            user=None, resolved_upstream=True)
+        self.assertSearchFinds(params, [])
+        # But if we set upstream bug tasks to "fix committed" or "fix
+        # released", the related bug tasks for our test target appear in
+        # the search result.
+        self.changeStatusOfBugTaskForOtherProduct(
+            self.bugtasks[0], BugTaskStatus.FIXCOMMITTED)
+        self.changeStatusOfBugTaskForOtherProduct(
+            self.bugtasks[1], BugTaskStatus.FIXRELEASED)
+        self.assertSearchFinds(params, self.bugtasks[:2])
+        # A search for bug tasks open upstream now returns only one
+        # test task.
+        params = self.getBugTaskSearchParams(user=None, open_upstream=True)
+        self.assertSearchFinds(params, self.bugtasks[2:])
+
+    def test_tags(self):
+        # Search results can be limited to bugs having given tags.
+        with person_logged_in(self.owner):
+            self.bugtasks[0].bug.tags = ['tag1', 'tag2']
+            self.bugtasks[1].bug.tags = ['tag1', 'tag3']
+        params = self.getBugTaskSearchParams(
+            user=None, tag=any('tag2', 'tag3'))
+        self.assertSearchFinds(params, self.bugtasks[:2])
+
+        params = self.getBugTaskSearchParams(
+            user=None, tag=all('tag2', 'tag3'))
+        self.assertSearchFinds(params, [])
+
+        params = self.getBugTaskSearchParams(
+            user=None, tag=all('tag1', 'tag3'))
+        self.assertSearchFinds(params, self.bugtasks[1:2])
+
+        params = self.getBugTaskSearchParams(
+            user=None, tag=all('tag1', '-tag3'))
+        self.assertSearchFinds(params, self.bugtasks[:1])
+
+        params = self.getBugTaskSearchParams(
+            user=None, tag=all('-tag1'))
+        self.assertSearchFinds(params, self.bugtasks[2:])
+
+        params = self.getBugTaskSearchParams(
+            user=None, tag=all('*'))
+        self.assertSearchFinds(params, self.bugtasks[:2])
+
+        params = self.getBugTaskSearchParams(
+            user=None, tag=all('-*'))
+        self.assertSearchFinds(params, self.bugtasks[2:])
+
+    def test_date_closed(self):
+        # Search results can be filtered by the date_closed time
+        # of a bugtask.
+        with person_logged_in(self.owner):
+            self.bugtasks[2].transitionToStatus(
+                BugTaskStatus.FIXRELEASED, self.owner)
+        utc_now = datetime.now(pytz.timezone('UTC'))
+        self.assertTrue(utc_now >= self.bugtasks[2].date_closed)
+        params = self.getBugTaskSearchParams(
+            user=None, date_closed=greater_than(utc_now-timedelta(days=1)))
+        self.assertSearchFinds(params, self.bugtasks[2:])
+        params = self.getBugTaskSearchParams(
+            user=None, date_closed=greater_than(utc_now+timedelta(days=1)))
+        self.assertSearchFinds(params, [])
+
+    def test_created_since(self):
+        # Search results can be limited to bugtasks created after a
+        # given time.
+        one_day_ago = self.bugtasks[0].datecreated - timedelta(days=1)
+        two_days_ago = self.bugtasks[0].datecreated - timedelta(days=2)
+        with person_logged_in(self.owner):
+            self.bugtasks[0].datecreated = two_days_ago
+        params = self.getBugTaskSearchParams(
+            user=None, created_since=one_day_ago)
+        self.assertSearchFinds(params, self.bugtasks[1:])
+
+    def test_modified_since(self):
+        # Search results can be limited to bugs modified after a
+        # given time.
+        one_day_ago = (
+            self.bugtasks[0].bug.date_last_updated - timedelta(days=1))
+        two_days_ago = (
+            self.bugtasks[0].bug.date_last_updated - timedelta(days=2))
+        with person_logged_in(self.owner):
+            self.bugtasks[0].bug.date_last_updated = two_days_ago
+        params = self.getBugTaskSearchParams(
+            user=None, modified_since=one_day_ago)
+        self.assertSearchFinds(params, self.bugtasks[1:])
+
+    def test_branches_linked(self):
+        # Search results can be limited to bugs with or without linked
+        # branches.
+        with person_logged_in(self.owner):
+            branch = self.factory.makeBranch()
+            self.bugtasks[0].bug.linkBranch(branch, self.owner)
+        params = self.getBugTaskSearchParams(
+            user=None, linked_branches=BugBranchSearch.BUGS_WITH_BRANCHES)
+        self.assertSearchFinds(params, self.bugtasks[:1])
+        params = self.getBugTaskSearchParams(
+            user=None, linked_branches=BugBranchSearch.BUGS_WITHOUT_BRANCHES)
+        self.assertSearchFinds(params, self.bugtasks[1:])
+
+    def test_limit_search_to_one_bug(self):
+        # Search results can be limited to a given bug.
+        params = self.getBugTaskSearchParams(
+            user=None, bug=self.bugtasks[0].bug)
+        self.assertSearchFinds(params, self.bugtasks[:1])
+        other_bug = self.factory.makeBug()
+        params = self.getBugTaskSearchParams(user=None, bug=other_bug)
+        self.assertSearchFinds(params, [])
+
+    def test_filter_by_status(self):
+        # Search results can be limited to bug tasks with a given status.
+        params = self.getBugTaskSearchParams(
+            user=None, status=BugTaskStatus.FIXCOMMITTED)
+        self.assertSearchFinds(params, self.bugtasks[2:])
+        params = self.getBugTaskSearchParams(
+            user=None, status=any(BugTaskStatus.NEW, BugTaskStatus.TRIAGED))
+        self.assertSearchFinds(params, self.bugtasks[:2])
+        params = self.getBugTaskSearchParams(
+            user=None, status=BugTaskStatus.WONTFIX)
+        self.assertSearchFinds(params, [])
+
+    def test_filter_by_importance(self):
+        # Search results can be limited to bug tasks with a given importance.
+        params = self.getBugTaskSearchParams(
+            user=None, importance=BugTaskImportance.HIGH)
+        self.assertSearchFinds(params, self.bugtasks[:1])
+        params = self.getBugTaskSearchParams(
+            user=None,
+            importance=any(BugTaskImportance.HIGH, BugTaskImportance.LOW))
+        self.assertSearchFinds(params, self.bugtasks[:2])
+        params = self.getBugTaskSearchParams(
+            user=None, importance=BugTaskImportance.MEDIUM)
+        self.assertSearchFinds(params, [])
+
+    def test_omit_duplicate_bugs(self):
+        # Duplicate bugs can optionally be excluded from search results.
+        # The default behaviour is to include duplicates.
+        duplicate_bug = self.bugtasks[0].bug
+        master_bug = self.bugtasks[1].bug
+        with person_logged_in(self.owner):
+            duplicate_bug.markAsDuplicate(master_bug)
+        params = self.getBugTaskSearchParams(user=None)
+        self.assertSearchFinds(params, self.bugtasks)
+        # If we explicitly pass the parameter omit_duplicates=False, we get
+        # the same result.
+        params = self.getBugTaskSearchParams(user=None, omit_dupes=False)
+        self.assertSearchFinds(params, self.bugtasks)
+        # If omit_duplicates is set to True, the first task bug is omitted.
+        params = self.getBugTaskSearchParams(user=None, omit_dupes=True)
+        self.assertSearchFinds(params, self.bugtasks[1:])
+
+    def test_has_cve(self):
+        # Search results can be limited to bugs linked to a CVE.
+        with person_logged_in(self.owner):
+            cve = self.factory.makeCVE('2010-0123')
+            self.bugtasks[0].bug.linkCVE(cve, self.owner)
+        params = self.getBugTaskSearchParams(user=None, has_cve=True)
+        self.assertSearchFinds(params, self.bugtasks[:1])
 
     def setUpFullTextSearchTests(self):
         # Set text fields indexed by Bug.fti, BugTask.fti or
@@ -404,10 +701,15 @@
         with person_logged_in(self.owner):
             self.bugtasks[0].bug.addNomination(nominator, series1)
             self.bugtasks[1].bug.addNomination(nominator, series2)
+<<<<<<< TREE
         params = self.getBugTaskSearchParams(user=None, nominated_for=series1)
         search_result = self.runSearch(params)
         expected = self.resultValuesForBugtasks(self.bugtasks[:1])
         self.assertEqual(expected, search_result)
+=======
+        params = self.getBugTaskSearchParams(user=None, nominated_for=series1)
+        self.assertSearchFinds(params, self.bugtasks[:1])
+>>>>>>> MERGE-SOURCE
 
 
 class BugTargetTestBase:
@@ -445,15 +747,12 @@
         supervisor = self.factory.makeTeam(owner=self.owner)
         params = self.getBugTaskSearchParams(
             user=None, bug_supervisor=supervisor)
-        search_result = self.runSearch(params)
-        self.assertEqual([], search_result)
+        self.assertSearchFinds(params, [])
 
         # If we appoint a bug supervisor, searching for bug tasks
         # by supervisor will return all bugs for our test target.
         self.setSupervisor(supervisor)
-        search_result = self.runSearch(params)
-        expected = self.resultValuesForBugtasks(self.bugtasks)
-        self.assertEqual(expected, search_result)
+        self.assertSearchFinds(params, self.bugtasks)
 
     def setSupervisor(self, supervisor):
         """Set the bug supervisor for the bug task target."""
@@ -484,6 +783,11 @@
         """See `ProductAndDistributionTests`."""
         return self.factory.makeProductSeries(product=self.searchtarget)
 
+    def findBugtaskForOtherProduct(self, bugtask):
+        # Return the bugtask for the product that not related to the
+        # main bug target.
+        return self._findBugtaskForOtherProduct(bugtask, self.searchtarget)
+
 
 class ProductSeriesTarget(BugTargetTestBase):
     """Use a product series as the bug target."""
@@ -503,6 +807,31 @@
         params.setProductSeries(self.searchtarget)
         return params
 
+    def changeStatusOfBugTaskForOtherProduct(self, bugtask, new_status):
+        # Change the status of another bugtask of the same bug to the
+        # given status.
+        #
+        # This method is called by SearchTestBase.test_upstream_status().
+        # A search for bugs which are open or closed upstream has an
+        # odd behaviour when the search target is a product series: In
+        # this case, all bugs with an open or closed bug task for _any_
+        # product are returned, including bug tasks for the main product
+        # of the series. Hence we must set the status for all products
+        # in order to avoid a failure of test_upstream_status().
+        bug = bugtask.bug
+        for other_task in bug.bugtasks:
+            other_target = other_task.target
+            if IProduct.providedBy(other_target):
+                with person_logged_in(other_target.owner):
+                    other_task.transitionToStatus(
+                        new_status, other_target.owner)
+
+    def findBugtaskForOtherProduct(self, bugtask):
+        # Return the bugtask for the product that not related to the
+        # main bug target.
+        return self._findBugtaskForOtherProduct(
+            bugtask, self.searchtarget.product)
+
 
 class ProjectGroupTarget(BugTargetTestBase, BugTargetWithBugSuperVisor):
     """Use a project group as the bug target."""
@@ -525,8 +854,10 @@
     def makeBugTasks(self):
         """Create bug tasks for the search target."""
         self.bugtasks = []
+        self.products = []
         with person_logged_in(self.owner):
             product = self.factory.makeProduct(owner=self.owner)
+            self.products.append(product)
             product.project = self.searchtarget
             self.bugtasks.append(
                 self.factory.makeBugTask(target=product))
@@ -535,6 +866,7 @@
                 BugTaskStatus.TRIAGED, self.owner)
 
             product = self.factory.makeProduct(owner=self.owner)
+            self.products.append(product)
             product.project = self.searchtarget
             self.bugtasks.append(
                 self.factory.makeBugTask(target=product))
@@ -543,6 +875,7 @@
             BugTaskStatus.NEW, self.owner)
 
             product = self.factory.makeProduct(owner=self.owner)
+            self.products.append(product)
             product.project = self.searchtarget
             self.bugtasks.append(
                 self.factory.makeBugTask(target=product))
@@ -557,6 +890,19 @@
             for bugtask in self.bugtasks:
                 bugtask.target.setBugSupervisor(supervisor, self.owner)
 
+    def findBugtaskForOtherProduct(self, bugtask):
+        # Return the bugtask for the product that not related to the
+        # main bug target.
+        bug = bugtask.bug
+        for other_task in bug.bugtasks:
+            other_target = other_task.target
+            if (IProduct.providedBy(other_target)
+                and other_target not in self.products):
+                return other_task
+        self.fail(
+            'No bug task found for a product that is not the target of '
+            'the main test bugtask.')
+
 
 class MilestoneTarget(BugTargetTestBase):
     """Use a milestone as the bug target."""
@@ -583,6 +929,11 @@
             for bugtask in self.bugtasks:
                 bugtask.transitionToMilestone(self.searchtarget, self.owner)
 
+    def findBugtaskForOtherProduct(self, bugtask):
+        # Return the bugtask for the product that not related to the
+        # main bug target.
+        return self._findBugtaskForOtherProduct(bugtask, self.product)
+
 
 class DistributionTarget(BugTargetTestBase, ProductAndDistributionTests,
                          BugTargetWithBugSuperVisor):
@@ -645,6 +996,14 @@
         params.setSourcePackage(self.searchtarget)
         return params
 
+    def subscribeToTarget(self, subscriber):
+        # Subscribe the given person to the search target.
+        # Source packages do not support structural subscriptions,
+        # so we subscribe to the distro series instead.
+        with person_logged_in(subscriber):
+            self.searchtarget.distroseries.addSubscription(
+                subscriber, subscribed_by=subscriber)
+
 
 class DistributionSourcePackageTarget(BugTargetTestBase,
                                       BugTargetWithBugSuperVisor):

=== modified file 'lib/lp/code/errors.py'
--- lib/lp/code/errors.py	2010-11-03 08:15:38 +0000
+++ lib/lp/code/errors.py	2010-11-03 16:36:06 +0000
@@ -133,13 +133,32 @@
     """The branch cannot be made private."""
 
 
-class CannotHaveLinkedBranch(Exception):
+class InvalidBranchException(Exception):
+    """Base exception for an error resolving a branch for a component.
+
+    Subclasses should set _msg_template to match their required display
+    message.
+    """
+
+    _msg_template = "Invalid branch for: %s"
+
+    def __init__(self, component):
+        self.component = component
+        # It's expected that components have a name attribute,
+        # so let's assume they will and deal with any error if it occurs.
+        try:
+            component_name = component.name
+        except AttributeError:
+            component_name = str(component)
+        # The display_message contains something readable for the user.
+        self.display_message = self._msg_template % component_name
+        Exception.__init__(self, self._msg_template % (repr(component),))
+
+
+class CannotHaveLinkedBranch(InvalidBranchException):
     """Raised when we try to get the linked branch for a thing that can't."""
 
-    def __init__(self, component):
-        self.component = component
-        Exception.__init__(
-            self, "%r cannot have linked branches." % (component,))
+    _msg_template = "%s cannot have linked branches."
 
 
 class ClaimReviewFailed(Exception):
@@ -172,12 +191,10 @@
             self, "Cannot understand namespace name: '%s'" % (name,))
 
 
-class NoLinkedBranch(Exception):
+class NoLinkedBranch(InvalidBranchException):
     """Raised when there's no linked branch for a thing."""
 
-    def __init__(self, component):
-        self.component = component
-        Exception.__init__(self, "%r has no linked branch." % (component,))
+    _msg_template = "%s has no linked branch."
 
 
 class NoSuchBranch(NameLookupFailed):

=== modified file 'lib/lp/code/windmill/tests/test_branch_broken_links.py'
--- lib/lp/code/windmill/tests/test_branch_broken_links.py	2010-11-03 08:15:38 +0000
+++ lib/lp/code/windmill/tests/test_branch_broken_links.py	2010-11-03 16:36:06 +0000
@@ -1,3 +1,4 @@
+<<<<<<< TREE
 # Copyright 2009 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
@@ -111,3 +112,134 @@
 
 def test_suite():
     return unittest.TestLoader().loadTestsFromName(__name__)
+=======
+# Copyright 2009 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Test for links between branches and bugs or specs."""
+
+__metaclass__ = type
+__all__ = []
+
+import unittest
+
+import transaction
+import windmill
+from zope.security.proxy import removeSecurityProxy
+
+from canonical.launchpad.windmill.testing.constants import SLEEP
+from lp.code.windmill.testing import CodeWindmillLayer
+from lp.testing import (
+    login,
+    WindmillTestCase,
+    )
+
+
+LOGIN_LINK = (
+    u'//div[@id="add-comment-login-first"]')
+
+
+class TestBranchLinks(WindmillTestCase):
+    """Test the rendering of broken branch links."""
+
+    layer = CodeWindmillLayer
+    suite_name = "Broken branch links"
+
+    BUG_TEXT_TEMPLATE = u"""
+    Here is the bug. Which branches are valid?
+    Valid: %s
+    Invalid %s
+    """
+
+    BRANCH_URL_TEMPLATE = "lp:%s"
+
+    def make_product_and_valid_links(self):
+        branch = self.factory.makeProductBranch()
+        valid_branch_url = self.BRANCH_URL_TEMPLATE % branch.unique_name
+        product = self.factory.makeProduct()
+        product_branch = self.factory.makeProductBranch(product=product)
+        naked_product = removeSecurityProxy(product)
+        naked_product.development_focus.branch = product_branch
+        valid_product_url = self.BRANCH_URL_TEMPLATE % product.name
+
+        return (naked_product, [
+            valid_branch_url,
+            valid_product_url,
+        ])
+
+    def make_invalid_links(self):
+        product = self.factory.makeProduct()
+        distro = self.factory.makeDistribution()
+        person = self.factory.makePerson()
+        branch = self.factory.makeBranch(private=True, owner=person)
+        naked_branch = removeSecurityProxy(branch)
+        return dict([
+            (self.BRANCH_URL_TEMPLATE % 'foo', "No such product: 'foo'."),
+            (self.BRANCH_URL_TEMPLATE % product.name,
+                "%s has no linked branch." % product.name),
+            (self.BRANCH_URL_TEMPLATE % ('%s/bar' % product.name),
+                "No such product series: 'bar'."),
+            (self.BRANCH_URL_TEMPLATE % ('%s' % naked_branch.unique_name),
+                "No such branch: '%s'." % naked_branch.unique_name),
+            (self.BRANCH_URL_TEMPLATE % distro.name,
+                "%s cannot have linked branches." % distro.name),
+            ])
+
+    def test_invalid_url_rendering(self):
+        naked_product, valid_links = self.make_product_and_valid_links()
+        invalid_links = self.make_invalid_links()
+
+        from lp.testing import ANONYMOUS
+        login(ANONYMOUS)
+        client = self.client
+        bug_description = self.BUG_TEXT_TEMPLATE % (
+            ', '.join(valid_links), ', '.join(invalid_links.keys()))
+        bug = self.factory.makeBug(product=naked_product,
+                                        title="The meaning of life is broken",
+                                        description=bug_description)
+        transaction.commit()
+
+        bug_url = (
+            windmill.settings['TEST_URL'] + '%s/+bug/%s'
+            % (naked_product.name, bug.id))
+        client.open(url=bug_url)
+        client.waits.forElement(xpath=LOGIN_LINK)
+
+        # Let the Ajax call run
+        client.waits.sleep(milliseconds=SLEEP)
+
+        code = """
+            var good_a = windmill.testWin().document.getElementsByClassName(
+                            'branch-short-link', 'a');
+            var good_links = [];
+            for( i=0; i<good_a.length; i++ ) {
+                good_links.push(good_a[i].innerHTML);
+            }
+
+            var bad_a = windmill.testWin().document.getElementsByClassName(
+                            'invalid-link', 'a');
+            var bad_links = {};
+            for( i=0; i<bad_a.length; i++ ) {
+                bad_links[bad_a[i].innerHTML] = bad_a[i].title;
+            }
+
+
+            var result = {};
+            result.good = good_links;
+            result.bad = bad_links;
+            result
+        """
+        raw_result = client.commands.execJS(js=code)
+        result = raw_result['result']
+        result_valid_links = result['good']
+        result_invalid_links = result['bad']
+        self.assertEqual(set(invalid_links.keys()),
+                         set(result_invalid_links.keys()))
+        for (href, title) in invalid_links.items():
+            self.assertEqual(title, result_invalid_links[href])
+        self.assertEqual(set(valid_links), set(result_valid_links))
+
+
+def test_suite():
+    return unittest.TestLoader().loadTestsFromName(__name__)
+>>>>>>> MERGE-SOURCE

=== modified file 'lib/lp/registry/browser/person.py'
=== modified file 'lib/lp/registry/browser/tests/milestone-views.txt'
=== modified file 'lib/lp/registry/configure.zcml'
=== added file 'lib/lp/registry/doc/mentoringoffer.txt.OTHER'
--- lib/lp/registry/doc/mentoringoffer.txt.OTHER	1970-01-01 00:00:00 +0000
+++ lib/lp/registry/doc/mentoringoffer.txt.OTHER	2010-11-03 16:36:06 +0000
@@ -0,0 +1,292 @@
+= The Mentoring System =
+
+== Offers of Mentoring ==
+
+Launchpad allows people to make an offer of mentoring for a bug or
+specification. Here, Foo Bar will make an offer on a spec and a bug:
+
+    >>> from lp.bugs.interfaces.bug import IBugSet
+    >>> from lp.registry.interfaces.distribution import IDistributionSet
+    >>> from lp.registry.interfaces.person import IPersonSet
+    >>> from canonical.launchpad.ftests import login, syncUpdate
+    >>> distroset = getUtility(IDistributionSet)
+    >>> personset = getUtility(IPersonSet)
+    >>> bugset = getUtility(IBugSet)
+    >>> foo_bar = personset.getByEmail('foo.bar@xxxxxxxxxxxxx')
+    >>> mark = personset.getByName('mark')
+    >>> lpteam = personset.getByName('launchpad')
+    >>> kubuntu = distroset.getByName('kubuntu')
+    >>> bug2 = bugset.get(2)
+    >>> bug2.is_complete
+    False
+    >>> spec1 = kubuntu.getSpecification('cluster-installation')
+    >>> spec1.is_complete
+    False
+    >>> login('foo.bar@xxxxxxxxxxxxx')
+    >>> offer1 = bug2.offerMentoring(foo_bar, lpteam)
+    >>> offer2 = spec1.offerMentoring(foo_bar, lpteam)
+
+Now, it should be possible to see those offers on a variety of lists.
+
+First, we look at foo_bar's offers:
+
+    >>> for offer in foo_bar.mentoring_offers:
+    ...     print offer.target.title
+    Facilitate mass installs  of Ubuntu using Netboot configuration
+    Blackhole Trash folder
+
+Now, let's look at the list of offers associated with lpteam. For this we
+use the team_mentorships attribute, because mentoring_offers would imply
+offers made *by* the team, which we don't do. The team_memberships attribute
+is for mentorships made by people specifically in support of new members
+wanting to join that specific team:
+
+    >>> for offer in lpteam.team_mentorships:
+    ...     print offer.target.title
+    Facilitate mass installs  of Ubuntu using Netboot configuration
+    Blackhole Trash folder
+
+And at the offers related to Bug #2:
+
+    >>> for offer in bug2.mentoring_offers:
+    ...     print offer.target.title
+    Blackhole Trash folder
+
+And for the specification concerned:
+
+    >>> for offer in spec1.mentoring_offers:
+    ...     print offer.target.title
+    Facilitate mass installs  of Ubuntu using Netboot configuration
+
+Now, Bug #2 is associated with both Ubuntu and Debian
+
+    >>> ubuntu = distroset.getByName('ubuntu')
+    >>> for offer in ubuntu.mentoring_offers:
+    ...     print offer.target.title
+    Blackhole Trash folder
+    >>> debian = distroset.getByName('debian')
+    >>> for offer in debian.mentoring_offers:
+    ...     print offer.target.title
+    Blackhole Trash folder
+
+It is also associated with the ubuntu *upstream* (this is a weirdness in the
+Launchpad sample data, we have an upstream product called Ubuntu).
+
+    >>> from lp.registry.interfaces.product import IProductSet
+    >>> productset = getUtility(IProductSet)
+    >>> tomcat = productset.getByName('tomcat')
+    >>> for offer in tomcat.mentoring_offers:
+    ...     print offer.target.title
+    Blackhole Trash folder
+
+Now, it gets more interesting.
+
+Let us add a INVALID bugtask for Bug #2 on the Firefox product.
+So the offer of mentoring is NOT relevant to Firefox, and should not
+show up there.
+
+    >>> firefox = productset.getByName('firefox')
+    >>> from lp.bugs.interfaces.bugtask import (
+    ...     BugTaskImportance,
+    ...     BugTaskStatus,
+    ...     IBugTaskSet,
+    ...     )
+    >>> bugtaskset = getUtility(IBugTaskSet)
+    >>> ff_task = bugtaskset.createTask(
+    ...     bug=bug2, product=firefox, owner=mark,
+    ...     status=BugTaskStatus.INVALID,
+    ...     importance=BugTaskImportance.MEDIUM)
+    >>> syncUpdate(ff_task)
+    >>> ff_task.product == firefox
+    True
+    >>> for task in bug2.bugtasks:
+    ...     if task.product == firefox:
+    ...         print 'Found firefox task with status', task.status.name
+    Found firefox task with status INVALID
+
+    >>> print firefox.mentoring_offers.count()
+    0
+
+We also can get lists of offers of mentoring associated with ProjectGroups.
+Tomcat is part of the Apache project group, so:
+
+    >>> from lp.registry.interfaces.projectgroup import IProjectGroupSet
+    >>> projectset = getUtility(IProjectGroupSet)
+    >>> apache = projectset.getByName('apache')
+    >>> for offer in apache.mentoring_offers:
+    ...     print offer.target.title
+    Blackhole Trash folder
+
+
+== The Mentorship Manager ==
+
+There is a utility which we can use to see all current mentorship offers
+across the whole system.
+
+    >>> from lp.registry.interfaces.mentoringoffer import IMentoringOfferSet
+    >>> mm = getUtility(IMentoringOfferSet)
+    >>> print mm.mentoring_offers.count()
+    2
+
+== Mentoring lists do not include private items ==
+
+When a bug is marked private, it drops off the mentoring lists.
+
+   >>> bug2.private
+   False
+   >>> bug2.setPrivate(True, getUtility(ILaunchBag).user)
+   True
+   >>> syncUpdate(bug2)
+   >>> bug2.private
+   True
+
+So, now that Bug #2 is private it should no longer appear in the list of
+mentorships offered by foo_bar:
+
+   >>> for offer in lpteam.team_mentorships:
+   ...     print offer.target.title
+   Facilitate mass installs  of Ubuntu using Netboot configuration
+
+Let's make sure it is off all of the lists:
+
+   >>> for offertarget in [foo_bar, apache, tomcat, debian, ubuntu, mm]:
+   ...     for offer in offertarget.mentoring_offers:
+   ...         if offer.target.title == 'Blackhole Trash folder':
+   ...             print 'Failed! Private bug showed in listing.'
+
+
+# NB these comments can be removed when we have private specs, which should
+# drop off the lists too.
+
+#And lets see if we can eliminate the spec by making that complete too.
+#
+#   >>> spec1.private
+#   False
+#   >>> spec1.private = True
+#   >>> syncUpdate(spec1)
+#   >>> spec1.private
+#   True
+#   >>> print foo_bar.mentoring_offers.count()
+#   0
+
+#And both offers should have disappeared from ALL the listings we looked at
+#earlier:
+#
+#   >>> print lpteam.team_mentorships.count()
+#   0
+#   >>> print uproj.mentoring_offers.count()
+#   0
+#   >>> print ubuntu_prod.mentoring_offers.count()
+#   0
+#   >>> print debian.mentoring_offers.count()
+#   0
+#   >>> print ubuntu.mentoring_offers.count()
+#   0
+
+#The mentorship manager should also no longer show those items:
+#
+#   >>> print mm.mentoring_offers.count()
+#   0
+
+
+OK, let's mark the bug public again:
+
+   >>> bug2.private
+   True
+   >>> bug2.setPrivate(False, getUtility(ILaunchBag).user)
+   True
+   >>> syncUpdate(bug2)
+   >>> bug2.private
+   False
+
+
+== Mentoring lists include only incomplete items ==
+
+When a spec or a bug is completed, it drops off the mentoring lists.
+
+   >>> from lp.blueprints.enums import SpecificationImplementationStatus
+   >>> from lp.bugs.interfaces.bugtask import BugTaskStatus
+   >>> bug2.is_complete
+   False
+   >>> from canonical.database.sqlbase import flush_database_updates
+   >>> for task in bug2.bugtasks:
+   ...   if task.conjoined_master is None:
+   ...     task.transitionToStatus(
+   ...       BugTaskStatus.FIXRELEASED, getUtility(ILaunchBag).user)
+   >>> flush_database_updates()
+   >>> bug2.is_complete
+   True
+
+So, now that Bug #2 is "complete" it should no longer appear in the list of
+mentorships offered by foo_bar:
+
+   >>> for offer in foo_bar.mentoring_offers:
+   ...     print offer.target.title
+   Facilitate mass installs  of Ubuntu using Netboot configuration
+
+And lets see if we can eliminate the spec by making that complete too.
+
+   >>> spec1.is_complete
+   False
+   >>> spec1.implementation_status = SpecificationImplementationStatus.IMPLEMENTED
+   >>> newstate = spec1.updateLifecycleStatus(foo_bar)
+   >>> syncUpdate(spec1)
+   >>> spec1.is_complete
+   True
+   >>> print foo_bar.mentoring_offers.count()
+   0
+
+And both offers should have disappeared from ALL the listings we looked at
+earlier:
+
+   >>> print lpteam.team_mentorships.count()
+   0
+   >>> print apache.mentoring_offers.count()
+   0
+   >>> print tomcat.mentoring_offers.count()
+   0
+   >>> print debian.mentoring_offers.count()
+   0
+   >>> print ubuntu.mentoring_offers.count()
+   0
+
+The mentorship manager should also no longer show those items:
+
+   >>> print mm.mentoring_offers.count()
+   0
+
+However, the mentorship manager should show these as recent successful
+mentorings:
+
+   >>> for offer in mm.recent_completed_mentorships:
+   ...     print offer.target.title
+   Blackhole Trash folder
+   Facilitate mass installs  of Ubuntu using Netboot configuration
+
+
+== Checking if the person is a Mentor ==
+
+Once an offer has been made, you can test to see if a person is a mentor for
+a bug or a blueprint.
+
+    >>> stevea = personset.getByName('stevea')
+    >>> bug2.isMentor(stevea)
+    False
+    >>> bug2.isMentor(foo_bar)
+    True
+    >>> spec1.isMentor(stevea)
+    False
+    >>> bug2.isMentor(foo_bar)
+    True
+
+== Retracting mentorship offers ==
+
+We can also retract offers of mentoring.
+
+    >>> bug2.retractMentoring(foo_bar)
+    >>> bug2.isMentor(foo_bar)
+    False
+    >>> spec1.retractMentoring(foo_bar)
+    >>> spec1.isMentor(foo_bar)
+    False
+

=== modified file 'lib/lp/registry/interfaces/distribution.py'
--- lib/lp/registry/interfaces/distribution.py	2010-11-02 20:10:56 +0000
+++ lib/lp/registry/interfaces/distribution.py	2010-11-03 16:36:06 +0000
@@ -129,9 +129,15 @@
 
 class IDistributionPublic(
     IBugTarget, ICanGetMilestonesDirectly, IHasAppointedDriver,
+<<<<<<< TREE
     IHasBuildRecords, IHasDrivers, IHasMilestones,
     IHasOwner, IHasSecurityContact, IHasSprints, IHasTranslationImports,
     ITranslationPolicy, IKarmaContext, ILaunchpadUsage, IMakesAnnouncements,
+=======
+    IHasBuildRecords, IHasDrivers, IHasMentoringOffers, IHasMilestones,
+    IHasOwner, IHasSecurityContact, IHasSprints, IHasTranslationImports,
+    ITranslationPolicy, IKarmaContext, ILaunchpadUsage, IMakesAnnouncements,
+>>>>>>> MERGE-SOURCE
     IOfficialBugTagTargetPublic, IPillar, IServiceUsage,
     ISpecificationTarget):
     """Public IDistribution properties."""

=== modified file 'lib/lp/registry/interfaces/person.py'
=== modified file 'lib/lp/registry/interfaces/product.py'
=== modified file 'lib/lp/registry/model/distribution.py'
=== modified file 'lib/lp/registry/model/person.py'
=== modified file 'lib/lp/registry/model/product.py'
=== modified file 'lib/lp/registry/model/projectgroup.py'
=== modified file 'lib/lp/registry/tests/test_person.py'
=== modified file 'lib/lp/services/features/tests/test_helpers.py'
--- lib/lp/services/features/tests/test_helpers.py	2010-11-02 13:37:42 +0000
+++ lib/lp/services/features/tests/test_helpers.py	2010-11-03 16:36:06 +0000
@@ -1,64 +1,134 @@
-# Copyright 2010 Canonical Ltd.  This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-"""Tests for the feature flags test helpers."""
-
-__metaclass__ = type
-__all__ = []
-
-from canonical.testing import layers
-from lp.testing import TestCase
-from lp.services.features import getFeatureFlag
-from lp.services.features.testing import FeatureFixture
-
-
-class TestFeaturesContextManager(TestCase):
-    """Tests for the feature flags context manager test helper."""
-
-    layer = layers.DatabaseFunctionalLayer
-
-    def test_setting_one_flag_with_manager(self):
-        flag = self.getUniqueString()
-        value_outside_manager = getFeatureFlag(flag)
-        value_in_manager = None
-
-        with FeatureFixture({flag: u'on'}):
-            value_in_manager = getFeatureFlag(flag)
-
-        self.assertEqual(value_in_manager, u'on')
-        self.assertEqual(value_outside_manager, getFeatureFlag(flag))
-        self.assertNotEqual(value_outside_manager, value_in_manager)
-
-
-class TestFeaturesFixture(TestCase):
-    """Tests for the feature flags test fixture."""
-
-    layer = layers.DatabaseFunctionalLayer
-
-    def test_fixture_sets_one_flag_and_cleans_up_again(self):
-        flag = self.getUniqueString()
-        value_before_fixture_setup = getFeatureFlag(flag)
-        value_after_fixture_setup = None
-
-        fixture = FeatureFixture({flag: 'on'})
-        fixture.setUp()
-        value_after_fixture_setup = getFeatureFlag(flag)
-        fixture.cleanUp()
-
-        self.assertEqual(value_after_fixture_setup, 'on')
-        self.assertEqual(value_before_fixture_setup, getFeatureFlag(flag))
-        self.assertNotEqual(
-            value_before_fixture_setup, value_after_fixture_setup)
-
-    def test_fixture_deletes_existing_values(self):
-        self.useFixture(FeatureFixture({'one': '1'}))
-        self.useFixture(FeatureFixture({'two': '2'}))
-
-        self.assertEqual(getFeatureFlag('one'), None)
-        self.assertEqual(getFeatureFlag('two'), u'2')
-
-    def test_fixture_overrides_previously_set_flags(self):
-        self.useFixture(FeatureFixture({'one': '1'}))
-        self.useFixture(FeatureFixture({'one': '5'}))
-
-        self.assertEqual(getFeatureFlag('one'), u'5')
+<<<<<<< TREE
+# Copyright 2010 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Tests for the feature flags test helpers."""
+
+__metaclass__ = type
+__all__ = []
+
+from canonical.testing import layers
+from lp.testing import TestCase
+from lp.services.features import getFeatureFlag
+from lp.services.features.testing import FeatureFixture
+
+
+class TestFeaturesContextManager(TestCase):
+    """Tests for the feature flags context manager test helper."""
+
+    layer = layers.DatabaseFunctionalLayer
+
+    def test_setting_one_flag_with_manager(self):
+        flag = self.getUniqueString()
+        value_outside_manager = getFeatureFlag(flag)
+        value_in_manager = None
+
+        with FeatureFixture({flag: u'on'}):
+            value_in_manager = getFeatureFlag(flag)
+
+        self.assertEqual(value_in_manager, u'on')
+        self.assertEqual(value_outside_manager, getFeatureFlag(flag))
+        self.assertNotEqual(value_outside_manager, value_in_manager)
+
+
+class TestFeaturesFixture(TestCase):
+    """Tests for the feature flags test fixture."""
+
+    layer = layers.DatabaseFunctionalLayer
+
+    def test_fixture_sets_one_flag_and_cleans_up_again(self):
+        flag = self.getUniqueString()
+        value_before_fixture_setup = getFeatureFlag(flag)
+        value_after_fixture_setup = None
+
+        fixture = FeatureFixture({flag: 'on'})
+        fixture.setUp()
+        value_after_fixture_setup = getFeatureFlag(flag)
+        fixture.cleanUp()
+
+        self.assertEqual(value_after_fixture_setup, 'on')
+        self.assertEqual(value_before_fixture_setup, getFeatureFlag(flag))
+        self.assertNotEqual(
+            value_before_fixture_setup, value_after_fixture_setup)
+
+    def test_fixture_deletes_existing_values(self):
+        self.useFixture(FeatureFixture({'one': '1'}))
+        self.useFixture(FeatureFixture({'two': '2'}))
+
+        self.assertEqual(getFeatureFlag('one'), None)
+        self.assertEqual(getFeatureFlag('two'), u'2')
+
+    def test_fixture_overrides_previously_set_flags(self):
+        self.useFixture(FeatureFixture({'one': '1'}))
+        self.useFixture(FeatureFixture({'one': '5'}))
+
+        self.assertEqual(getFeatureFlag('one'), u'5')
+=======
+# Copyright 2010 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Tests for the feature flags test helpers."""
+
+from __future__ import with_statement
+
+
+__metaclass__ = type
+__all__ = []
+
+from canonical.testing import layers
+from lp.testing import TestCase
+from lp.services.features import getFeatureFlag
+from lp.services.features.testing import FeatureFixture
+
+
+class TestFeaturesContextManager(TestCase):
+    """Tests for the feature flags context manager test helper."""
+
+    layer = layers.DatabaseFunctionalLayer
+
+    def test_setting_one_flag_with_manager(self):
+        flag = self.getUniqueString()
+        value_outside_manager = getFeatureFlag(flag)
+        value_in_manager = None
+
+        with FeatureFixture({flag: u'on'}):
+            value_in_manager = getFeatureFlag(flag)
+
+        self.assertEqual(value_in_manager, u'on')
+        self.assertEqual(value_outside_manager, getFeatureFlag(flag))
+        self.assertNotEqual(value_outside_manager, value_in_manager)
+
+
+class TestFeaturesFixture(TestCase):
+    """Tests for the feature flags test fixture."""
+
+    layer = layers.DatabaseFunctionalLayer
+
+    def test_fixture_sets_one_flag_and_cleans_up_again(self):
+        flag = self.getUniqueString()
+        value_before_fixture_setup = getFeatureFlag(flag)
+        value_after_fixture_setup = None
+
+        fixture = FeatureFixture({flag: 'on'})
+        fixture.setUp()
+        value_after_fixture_setup = getFeatureFlag(flag)
+        fixture.cleanUp()
+
+        self.assertEqual(value_after_fixture_setup, 'on')
+        self.assertEqual(value_before_fixture_setup, getFeatureFlag(flag))
+        self.assertNotEqual(
+            value_before_fixture_setup, value_after_fixture_setup)
+
+    def test_fixture_deletes_existing_values(self):
+        self.useFixture(FeatureFixture({'one': '1'}))
+        self.useFixture(FeatureFixture({'two': '2'}))
+
+        self.assertEqual(getFeatureFlag('one'), None)
+        self.assertEqual(getFeatureFlag('two'), u'2')
+
+    def test_fixture_overrides_previously_set_flags(self):
+        self.useFixture(FeatureFixture({'one': '1'}))
+        self.useFixture(FeatureFixture({'one': '5'}))
+
+        self.assertEqual(getFeatureFlag('one'), u'5')
+>>>>>>> MERGE-SOURCE

=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py	2010-11-03 10:35:12 +0000
+++ lib/lp/testing/factory.py	2010-11-03 16:36:06 +0000
@@ -111,6 +111,10 @@
     IBugTrackerSet,
     )
 from lp.bugs.interfaces.bugwatch import IBugWatchSet
+from lp.bugs.interfaces.cve import (
+    CveStatus,
+    ICveSet,
+    )
 from lp.buildmaster.enums import (
     BuildFarmJobType,
     BuildStatus,
@@ -3227,6 +3231,13 @@
             consumer, reviewed_by=owner, access_level=access_level)
         return request_token.createAccessToken()
 
+    def makeCVE(self, sequence, description=None,
+                cvestate=CveStatus.CANDIDATE):
+        """Create a new CVE record."""
+        if description is None:
+            description = self.getUniqueString()
+        return getUtility(ICveSet).new(sequence, description, cvestate)
+
 
 # Some factory methods return simple Python types. We don't add
 # security wrappers for them, as well as for objects created by

=== modified file 'lib/lp/translations/doc/potmsgset.txt'
=== modified file 'lib/lp/translations/model/potmsgset.py'
=== modified file 'lib/lp/translations/tests/test_translationimportqueue.py'
=== modified file 'utilities/migrater/file-ownership.txt'
=== modified file 'versions.cfg'