launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #01800
[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'