← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~adeuring/launchpad/bug-739052-4 into lp:launchpad

 

Abel Deuring has proposed merging lp:~adeuring/launchpad/bug-739052-4 into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~adeuring/launchpad/bug-739052-4/+merge/70717

This branch changes the clauses generated by StormResultSet for slicing so that adjacent order columns having the same sort direction are grouped into (col1, col2...) > (memo1, memo2...) expressions.

I also renamed DecoratedResutlSet.getPlainResultSet() to get_plain_result_set; this method now works with nested DecoratedResultSets.

tests:

./bin/test canonical -vvt canonical.launchpad.webapp.tests.test_batching
./bin/test canonical -vvt decoratedresultset.txt

no lint
-- 
https://code.launchpad.net/~adeuring/launchpad/bug-739052-4/+merge/70717
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~adeuring/launchpad/bug-739052-4 into lp:launchpad.
=== modified file 'lib/canonical/launchpad/components/decoratedresultset.py'
--- lib/canonical/launchpad/components/decoratedresultset.py	2011-07-28 11:13:27 +0000
+++ lib/canonical/launchpad/components/decoratedresultset.py	2011-08-08 10:56:38 +0000
@@ -174,9 +174,12 @@
             new_result_set, self.result_decorator, self.pre_iter_hook,
             self.slice_info)
 
-    def getPlainResultSet(self):
+    def get_plain_result_set(self):
         """Return the plain Storm result set."""
-        return self.result_set
+        if isinstance(self.result_set, DecoratedResultSet):
+            return self.result_set.get_plain_result_set()
+        else:
+            return self.result_set
 
     def find(self, *args, **kwargs):
         """See `IResultSet`.

=== modified file 'lib/canonical/launchpad/components/tests/decoratedresultset.txt'
--- lib/canonical/launchpad/components/tests/decoratedresultset.txt	2011-07-28 11:13:27 +0000
+++ lib/canonical/launchpad/components/tests/decoratedresultset.txt	2011-08-08 10:56:38 +0000
@@ -152,3 +152,20 @@
     u'Dist name is: ubuntu'
     u'Dist name is: ubuntutest'
 
+
+== get_plain_result_set() ==
+
+DecoratedResultSet.get_plain_result_set() returns the plain Storm result
+set.
+
+    >>> decorated_result_set.get_plain_result_set()
+    <storm.store.ResultSet object at...
+
+get_plain_result_set() works for nested DecoratedResultSets.
+
+    >>> def embellish(result):
+    ...     return result.replace('Dist name', 'The distribution name')
+    >>> embellished_result_set = DecoratedResultSet(
+    ...     decorated_result_set, embellish)
+    >>> embellished_result_set.get_plain_result_set()
+    <storm.store.ResultSet object at...

=== modified file 'lib/canonical/launchpad/webapp/batching.py'
--- lib/canonical/launchpad/webapp/batching.py	2011-08-03 10:16:22 +0000
+++ lib/canonical/launchpad/webapp/batching.py	2011-08-08 10:56:38 +0000
@@ -200,7 +200,7 @@
         """
         self.resultset = resultset
         if zope_isinstance(resultset, DecoratedResultSet):
-            self.plain_resultset = resultset.getPlainResultSet()
+            self.plain_resultset = resultset.get_plain_result_set()
         else:
             self.plain_resultset = resultset
         self.error_cb = error_cb
@@ -334,6 +334,7 @@
 
         return [
             invert_sort_expression(expression)
+<<<<<<< TREE
             for expression in self.getOrderBy()]
 
     def andClausesForLeadingColumns(self, limits):
@@ -431,6 +432,141 @@
         naked_result = removeSecurityProxy(self.resultset).find(where)
         result = ProxyFactory(naked_result)
         return result.config(limit=size)
+=======
+            for expression in self.getOrderBy()]
+
+    def limitsGroupedByOrderDirection(self, sort_expressions, memos):
+        """Group sort expressions and memo values by order direction."""
+        descending = isinstance(sort_expressions[0], Desc)
+        grouped_limits = []
+        expression_group = []
+        memo_group = []
+        for expression, memo in zip(sort_expressions, memos):
+            if descending == isinstance(expression, Desc):
+                expression_group.append(expression)
+                memo_group.append(memo)
+            else:
+                grouped_limits.append((expression_group, memo_group))
+                descending = isinstance(expression, Desc)
+                expression_group = [expression]
+                memo_group = [memo]
+        grouped_limits.append((expression_group, memo_group))
+        return grouped_limits
+
+    def lessThanOrGreaterThanExpression(self, expressions, memos):
+        """Return an SQL expression "(expressions) OP (memos)".
+
+        OP is >, if the elements of expressions are PropertyColumns; else
+        the elements of expressions are instances of Desc(PropertyColumn)
+        and OP is <.
+        """
+        descending = isinstance(expressions[0], Desc)
+        if descending:
+            expressions = [expression.expr for expression in expressions]
+        expressions = map(compile, expressions)
+        expressions = ', '.join(expressions)
+        memos = ', '.join(sqlvalues(*memos))
+        if descending:
+            return SQL('(%s) < (%s)' % (expressions, memos))
+        else:
+            return SQL('(%s) > (%s)' % (expressions, memos))
+
+    def equalsExpressionsFromLimits(self, limits):
+        """Return a list [expression == memo, ...] for the given limits."""
+        def plain_expression(expression):
+            if isinstance(expression, Desc):
+                return expression.expr
+            else:
+                return expression
+
+        result = []
+        for expressions, memos in limits:
+            result.extend(
+                plain_expression(expression) == memo
+                for expression, memo in zip(expressions, memos))
+        return result
+
+    def whereExpressionsFromGroupedLimits(self, limits):
+        """Build a sequence of WHERE expressions from the given limits.
+
+        limits is a list of tuples (expressions, memos), where
+        expressions is a list of PropertyColumn instances or of
+        instances of Desc(PropertyColumn). Desc(PropertyColumn)
+        and PropertyColumn instances must not appear in the same
+        expressions list.
+
+        memos are the memo values asociated with the columns in
+        expressions.
+
+        Given a limits value of
+            [([c11, c12 ...], [m11, m12 ...]),
+             ([c21, c22 ...], [m21, m22 ...]),
+             ...
+             ([cN1, cN2 ...], [mN1, mN2 ...])]
+
+        this method returns a sequence of these Storm/SQL expressions:
+
+            * (c11, c12 ...) = (m11, m12 ...) AND
+              (c21, c22 ...) = (m21, m22 ...) AND
+              ...
+              (cN1, cN2 ...) < (mN1, mN2 ...)
+            * (c11, c12 ...) = (m11, m12 ...) AND
+              (c21, c22 ...) = (m21, m22 ...) AND
+              ...
+              (cM1, cM2 ...) < (mM1, mM2 ...)
+
+              (where M = N -1)
+            ...
+            * (c11, c12 ...) < (m11, m12 ...)
+
+        The getSlice() should return rows matching any of these
+        expressions. Note that the result sets returned by each
+        expression are disjuct, hence they can be simply ORed,
+        as well as used in a UNION ALL query.
+        """
+        start = limits[:-1]
+        last_expressions, last_memos = limits[-1]
+        last_clause = self.lessThanOrGreaterThanExpression(
+            last_expressions, last_memos)
+        if len(start) > 0:
+            clauses = self.equalsExpressionsFromLimits(start)
+            clauses.append(last_clause)
+            clauses = reduce(And, clauses)
+            return [clauses] + self.whereExpressionsFromGroupedLimits(start)
+        else:
+            return [last_clause]
+
+    def whereExpressions(self, sort_expressions, memos):
+        """WHERE expressions for the given sort columns and memos values."""
+        grouped_limits = self.limitsGroupedByOrderDirection(
+            sort_expressions, memos)
+        return self.whereExpressionsFromGroupedLimits(grouped_limits)
+
+    def getSliceFromMemo(self, size, memo):
+        """Return a result set for the given memo values.
+
+        Note that at least two other implementations are possible:
+        Instead of OR-combining the expressions returned by
+        whereExpressions(), these expressions could be used for
+        separate SELECTs which are then merged with UNION ALL.
+
+        We could also issue separate Storm queries for each
+        expression and combine the results here.
+
+        Which variant is more efficient is yet unknown; it may
+        differ between different queries.
+        """
+        sort_expressions = self.getOrderBy()
+        where = self.whereExpressions(sort_expressions, memo)
+        where = reduce(Or, where)
+        # From storm.zope.interfaces.IResultSet.__doc__:
+        #     - C{find()}, C{group_by()} and C{having()} are really
+        #       used to configure result sets, so are mostly intended
+        #       for use on the model side.
+        naked_result = removeSecurityProxy(self.resultset).find(where)
+        result = ProxyFactory(naked_result)
+        return result.config(limit=size)
+>>>>>>> MERGE-SOURCE
 
     def getSlice(self, size, endpoint_memo='', forwards=True):
         """See `IRangeFactory`."""

=== modified file 'lib/canonical/launchpad/webapp/tests/test_batching.py'
--- lib/canonical/launchpad/webapp/tests/test_batching.py	2011-08-03 10:16:22 +0000
+++ lib/canonical/launchpad/webapp/tests/test_batching.py	2011-08-08 10:56:38 +0000
@@ -4,6 +4,7 @@
 __metaclass__ = type
 
 from datetime import datetime
+import pytz
 import simplejson
 from unittest import TestLoader
 
@@ -135,7 +136,7 @@
         # of any Storm table class instance which appear in a result row.
         resultset = self.makeDecoratedStormResultSet()
         resultset = resultset.order_by(LibraryFileAlias.id)
-        plain_resultset = resultset.getPlainResultSet()
+        plain_resultset = resultset.get_plain_result_set()
         range_factory = StormRangeFactory(resultset)
         self.assertEqual(
             [plain_resultset[0][1].id],
@@ -211,9 +212,10 @@
         range_factory = StormRangeFactory(resultset)
         first, last = range_factory.getEndpointMemos(batchnav.batch)
         expected_first = simplejson.dumps(
-            [resultset.getPlainResultSet()[0][1].id], cls=DateTimeJSONEncoder)
+            [resultset.get_plain_result_set()[0][1].id],
+            cls=DateTimeJSONEncoder)
         expected_last = simplejson.dumps(
-            [resultset.getPlainResultSet()[2][1].id],
+            [resultset.get_plain_result_set()[2][1].id],
             cls=DateTimeJSONEncoder)
         self.assertEqual(expected_first, first)
         self.assertEqual(expected_last, last)
@@ -352,6 +354,7 @@
         self.assertIs(Person.id, reverse_person_id.expr)
         self.assertIs(Person.name, person_name)
 
+<<<<<<< TREE
     def test_whereExpressions__asc(self):
         """For ascending sort order, whereExpressions() returns the
         WHERE clause expression > memo.
@@ -419,6 +422,139 @@
         self.assertEquals(
             'Person.id = ? AND Person.name < ?', compile(where_clause_1))
         self.assertEquals('Person.id > ?', compile(where_clause_2))
+=======
+    def test_limitsGroupedByOrderDirection(self):
+        # limitsGroupedByOrderDirection() returns a sequence of
+        # (expressions, memos), where expressions is a list of
+        # ORDER BY expressions which either are all instances of
+        # PropertyColumn, or are all instances of Desc(PropertyColumn).
+        # memos are the related limit values.
+        range_factory = StormRangeFactory(None, self.logError)
+        order_by = [
+            Person.id, Person.datecreated, Person.name, Person.displayname]
+        limits = [1, datetime(2011, 07, 25, 0, 0, 0), 'foo', 'bar']
+        result = range_factory.limitsGroupedByOrderDirection(order_by, limits)
+        self.assertEqual([(order_by, limits)], result)
+        order_by = [
+            Desc(Person.id), Desc(Person.datecreated), Desc(Person.name),
+            Desc(Person.displayname)]
+        result = range_factory.limitsGroupedByOrderDirection(order_by, limits)
+        self.assertEqual([(order_by, limits)], result)
+        order_by = [
+            Person.id, Person.datecreated, Desc(Person.name),
+            Desc(Person.displayname)]
+        result = range_factory.limitsGroupedByOrderDirection(order_by, limits)
+        self.assertEqual(
+            [(order_by[:2], limits[:2]), (order_by[2:], limits[2:])], result)
+
+    def test_lessThanOrGreaterThanExpression__asc(self):
+        # beforeOrAfterExpression() returns an expression
+        # (col1, col2,..) > (memo1, memo2...) for ascending sort order.
+        range_factory = StormRangeFactory(None, self.logError)
+        expressions = [Person.id, Person.name]
+        limits = [1, 'foo']
+        limit_expression = range_factory.lessThanOrGreaterThanExpression(
+            expressions, limits)
+        self.assertEqual(
+            "(Person.id, Person.name) > (1, 'foo')",
+            compile(limit_expression))
+
+    def test_lessThanOrGreaterThanExpression__desc(self):
+        # beforeOrAfterExpression() returns an expression
+        # (col1, col2,..) < (memo1, memo2...) for descending sort order.
+        range_factory = StormRangeFactory(None, self.logError)
+        expressions = [Desc(Person.id), Desc(Person.name)]
+        limits = [1, 'foo']
+        limit_expression = range_factory.lessThanOrGreaterThanExpression(
+            expressions, limits)
+        self.assertEqual(
+            "(Person.id, Person.name) < (1, 'foo')",
+            compile(limit_expression))
+
+    def test_equalsExpressionsFromLimits(self):
+        range_factory = StormRangeFactory(None, self.logError)
+        order_by = [
+            Person.id, Person.datecreated, Desc(Person.name),
+            Desc(Person.displayname)]
+        limits = [
+            1, datetime(2011, 07, 25, 0, 0, 0, tzinfo=pytz.UTC), 'foo', 'bar']
+        limits = range_factory.limitsGroupedByOrderDirection(order_by, limits)
+        equals_expressions = range_factory.equalsExpressionsFromLimits(limits)
+        equals_expressions = map(compile, equals_expressions)
+        self.assertEqual(
+            ['Person.id = ?', 'Person.datecreated = ?', 'Person.name = ?',
+             'Person.displayname = ?'],
+            equals_expressions)
+
+    def test_whereExpressions__asc(self):
+        """For ascending sort order, whereExpressions() returns the
+        WHERE clause expression > memo.
+        """
+        resultset = self.makeStormResultSet()
+        range_factory = StormRangeFactory(resultset, self.logError)
+        [where_clause] = range_factory.whereExpressions([Person.id], [1])
+        self.assertEquals('(Person.id) > (1)', compile(where_clause))
+
+    def test_whereExpressions_desc(self):
+        """For descending sort order, whereExpressions() returns the
+        WHERE clause expression < memo.
+        """
+        resultset = self.makeStormResultSet()
+        range_factory = StormRangeFactory(resultset, self.logError)
+        [where_clause] = range_factory.whereExpressions(
+            [Desc(Person.id)], [1])
+        self.assertEquals('(Person.id) < (1)', compile(where_clause))
+
+    def test_whereExpressions__two_sort_columns_asc_asc(self):
+        """If the ascending sort columns c1, c2 and the memo values
+        m1, m2 are specified, whereExpressions() returns a WHERE
+        expressions comparing the tuple (c1, c2) with the memo tuple
+        (m1, m2):
+
+        (c1, c2) > (m1, m2)
+        """
+        resultset = self.makeStormResultSet()
+        range_factory = StormRangeFactory(resultset, self.logError)
+        [where_clause] = range_factory.whereExpressions(
+            [Person.id, Person.name], [1, 'foo'])
+        self.assertEquals(
+            "(Person.id, Person.name) > (1, 'foo')", compile(where_clause))
+
+    def test_whereExpressions__two_sort_columns_desc_desc(self):
+        """If the descending sort columns c1, c2 and the memo values
+        m1, m2 are specified, whereExpressions() returns a WHERE
+        expressions comparing the tuple (c1, c2) with the memo tuple
+        (m1, m2):
+
+        (c1, c2) < (m1, m2)
+        """
+        resultset = self.makeStormResultSet()
+        range_factory = StormRangeFactory(resultset, self.logError)
+        [where_clause] = range_factory.whereExpressions(
+            [Desc(Person.id), Desc(Person.name)], [1, 'foo'])
+        self.assertEquals(
+            "(Person.id, Person.name) < (1, 'foo')", compile(where_clause))
+
+    def test_whereExpressions__two_sort_columns_asc_desc(self):
+        """If the ascending sort column c1, the descending sort column
+        c2 and the memo values m1, m2 are specified, whereExpressions()
+        returns two expressions where  the first expression is
+
+            c1 == m1 AND c2 < m2
+
+        and the second expression is
+
+            c1 > m1
+        """
+        resultset = self.makeStormResultSet()
+        range_factory = StormRangeFactory(resultset, self.logError)
+        [where_clause_1, where_clause_2] = range_factory.whereExpressions(
+            [Person.id, Desc(Person.name)], [1, 'foo'])
+        self.assertEquals(
+            "Person.id = ? AND ((Person.name) < ('foo'))",
+            compile(where_clause_1))
+        self.assertEquals('(Person.id) > (1)', compile(where_clause_2))
+>>>>>>> MERGE-SOURCE
 
     def test_getSlice__forward_without_memo(self):
         resultset = self.makeStormResultSet()
@@ -499,7 +635,7 @@
         resultset = self.makeDecoratedStormResultSet()
         resultset.order_by(LibraryFileAlias.id)
         all_results = list(resultset)
-        memo = simplejson.dumps([resultset.getPlainResultSet()[0][1].id])
+        memo = simplejson.dumps([resultset.get_plain_result_set()[0][1].id])
         range_factory = StormRangeFactory(resultset)
         sliced_result = range_factory.getSlice(3, memo)
         self.assertEqual(all_results[1:4], list(sliced_result))

=== modified file 'lib/canonical/launchpad/zcml/decoratedresultset.zcml'
--- lib/canonical/launchpad/zcml/decoratedresultset.zcml	2011-07-21 15:59:49 +0000
+++ lib/canonical/launchpad/zcml/decoratedresultset.zcml	2011-08-08 10:56:38 +0000
@@ -10,7 +10,7 @@
 
     <class class="canonical.launchpad.components.decoratedresultset.DecoratedResultSet">
         <allow interface="storm.zope.interfaces.IResultSet" />
-        <allow attributes="__getslice__ getPlainResultSet" />
+        <allow attributes="__getslice__ get_plain_result_set" />
     </class>
 
 </configure>

=== modified file 'lib/lp/app/javascript/picker/picker.js'
--- lib/lp/app/javascript/picker/picker.js	2011-08-04 23:49:37 +0000
+++ lib/lp/app/javascript/picker/picker.js	2011-08-08 10:56:38 +0000
@@ -55,6 +55,7 @@
     FOOTER_SLOT = 'footer_slot',
     SELECTED_BATCH = 'selected_batch',
     SEARCH_MODE = 'search_mode',
+    NUM_SEARCHES = 'num_searches',
     NO_RESULTS_SEARCH_MESSAGE = 'no_results_search_message',
     RENDERUI = "renderUI",
     BINDUI = "bindUI",
@@ -698,7 +699,13 @@
         // clear the search mode.
         this.after('resultsChange', function (e) {
             this._syncResultsUI();
-            this.set(SEARCH_MODE, false);
+            this.set(NUM_SEARCHES, this.get(NUM_SEARCHES)-1);
+            this.set(SEARCH_MODE, this._isSearchOngoing());
+        }, this);
+
+        this.after('search', function (e) {
+            this.set(NUM_SEARCHES, this.get(NUM_SEARCHES)+1);
+            this.set(SEARCH_MODE, this._isSearchOngoing());
         }, this);
 
         // Update the search slot box whenever the "search_slot" property
@@ -813,7 +820,17 @@
      */
     _defaultSearch: function(e) {
         this.set(ERROR, null);
-        this.set(SEARCH_MODE, true);
+        this.set(SEARCH_MODE, this._isSearchOngoing());
+    },
+
+    /**
+     * Are there any outstanding searches at the moment?
+     *
+     * @method _isSearchOngoing
+     * @protected
+     */
+    _isSearchOngoing: function() {
+        return this.get(NUM_SEARCHES) !== 0;
     },
 
     /**
@@ -846,17 +863,6 @@
         if ( this.get('clear_on_save') ) {
             this._clear();
         }
-    },
-
-    /**
-     * By default, the select-batch event turns on search-mode.
-     *
-     * @method _defaultSelectBatch
-     * @param e {Event.Facade} An Event Facade object.
-     * @protected
-     */
-    _defaultSelectBatch: function(e) {
-        this.set(SEARCH_MODE, true);
     }
     });
 
@@ -1055,6 +1061,14 @@
     search_mode: { value: false },
 
     /**
+     * The current number of outstanding searches.
+     *
+     * @attribute num_searches
+     * @type Integer
+     */
+    num_searches: { value: 0 },
+
+    /**
      * The current error message. This puts the widget in 'error-mode',
      * setting this value to null clears that state.
      *

=== modified file 'lib/lp/app/javascript/picker/tests/test_picker.js'
--- lib/lp/app/javascript/picker/tests/test_picker.js	2011-08-04 23:56:26 +0000
+++ lib/lp/app/javascript/picker/tests/test_picker.js	2011-08-08 10:56:38 +0000
@@ -565,10 +565,30 @@
             'CSS class should be removed from the widget.');
     },
 
+    test_simultanious_searches: function () {
+        // It is possible that an automated search will overlap (temporally)
+        // with a user-initiated search.  If so, the picker will stay in
+        // search mode until until all outstanding searches are finished.
+        this.picker.render();
+        // Show that we start with search mode disabled.
+        Assert.isFalse(this.picker.get('search_mode'));
+        this.picker.fire('search', 1);
+        this.picker.fire('search', 2);
+        // We should be in search mode now.
+        Assert.isTrue(this.picker.get('search_mode'));
+        // If one of the searches gets results...
+        this.picker.set('results', []);
+        // ...we're still in search mode.
+        Assert.isTrue(this.picker.get('search_mode'));
+        // If the second search gets results, then we're out of search mode.
+        this.picker.set('results', []);
+        Assert.isFalse(this.picker.get('search_mode'));
+    },
+
     test_set_results_remove_search_mode: function () {
         this.picker.render();
 
-        this.picker.set('search_mode', true);
+        this.picker.fire('search');
         this.picker.set('results', []);
 
         Assert.isFalse(

=== modified file 'lib/lp/app/javascript/privacy.js'
--- lib/lp/app/javascript/privacy.js	2011-08-03 18:12:27 +0000
+++ lib/lp/app/javascript/privacy.js	2011-08-08 10:56:38 +0000
@@ -6,6 +6,7 @@
  *
  * This should be called after the page has loaded e.g. on 'domready'.
  */
+<<<<<<< TREE
 
 function setup_privacy_notification(config) {
     var notification_text = 'The information on this page is private';
@@ -86,6 +87,100 @@
         fade_in.run();
         body_space.run();
         login_space.run();
+=======
+
+function setup_privacy_notification(config) {
+    var notification_text = 'The information on this page is private';
+    var hidden = true;
+    var target_id = "maincontent";
+    if (config !== undefined) {
+        if (config.notification_text !== undefined) {
+            notification_text = config.notification_text;
+        }
+        if (config.hidden !== undefined) {
+            hidden = config.hidden;
+        }
+        if (config.target_id !== undefined) {
+            target_id = config.target_id;
+        }
+    }
+    var id_selector = "#" + target_id;
+    var main = Y.one(id_selector);
+    var notification = Y.Node.create('<div></div>')
+        .addClass('global-notification');
+    if (hidden) {
+        notification.addClass('hidden');
+    }
+    var notification_span = Y.Node.create('<span></span>')
+        .addClass('sprite')
+        .addClass('notification-private');
+    var close_link = Y.Node.create('<a></a>')
+        .addClass('global-notification-close')
+        .set('href', '#');
+    var close_span = Y.Node.create('<span></span>')
+        .addClass('sprite')
+        .addClass('notification-close');
+
+    notification.set('text', notification_text);
+    close_link.set('text', "Hide");
+
+    main.appendChild(notification);
+    notification.appendChild(notification_span);
+    notification.appendChild(close_link);
+    close_link.appendChild(close_span);
+
+}
+namespace.setup_privacy_notification = setup_privacy_notification;
+
+function display_privacy_notification(highlight_portlet_on_close) {
+    /* Check if the feature flag is set for this notification. */
+    var highlight = true;
+    if (highlight_portlet_on_close !== undefined) {
+        highlight = highlight_portlet_on_close;
+    }
+    if (privacy_notification_enabled) {
+        /* Set a temporary class on the body for the feature flag,
+         this is because we have no way to use feature flags in
+         css directly. This should be removed if the feature
+         is accepted. */
+        var body = Y.one('body');
+        body.addClass('feature-flag-bugs-private-notification-enabled');
+        /* Set the visible flag so that the content moves down. */
+        body.addClass('global-notification-visible');
+
+        var global_notification = Y.one('.global-notification');
+        if (global_notification.hasClass('hidden')) {
+            global_notification.addClass('transparent');
+            global_notification.removeClass('hidden');
+
+            var fade_in = new Y.Anim({
+                node: global_notification,
+                to: {opacity: 1},
+                duration: 0.3
+            });
+            var body_space = new Y.Anim({
+                node: 'body',
+                to: {'paddingTop': '40px'},
+                duration: 0.2,
+                easing: Y.Easing.easeOut
+            });
+            var login_space = new Y.Anim({
+                node: '.login-logout',
+                to: {'top': '45px'},
+                duration: 0.2,
+                easing: Y.Easing.easeOut
+            });
+
+            fade_in.run();
+            body_space.run();
+            login_space.run();
+        }
+
+        Y.one('.global-notification-close').on('click', function(e) {
+            hide_privacy_notification(highlight);
+            e.halt();
+        });
+>>>>>>> MERGE-SOURCE
     }
 
     Y.one('.global-notification-close').on('click', function(e) {

=== modified file 'lib/lp/app/templates/base-layout-macros.pt'
--- lib/lp/app/templates/base-layout-macros.pt	2011-08-03 15:26:36 +0000
+++ lib/lp/app/templates/base-layout-macros.pt	2011-08-08 10:56:38 +0000
@@ -106,6 +106,7 @@
 
   <metal:load-lavascript use-macro="context/@@+base-layout-macros/load-javascript" />
 
+<<<<<<< TREE
     <tal:if condition="request/features/bugs.private_notification.enabled">
     <script type="text/javascript">
       LPS.use('base', 'node', 'oop', 'event', 'lp.bugs.bugtask_index',
@@ -122,6 +123,19 @@
     </script>
     </tal:if>
 
+=======
+  <tal:if condition="request/features/bugs.private_notification.enabled">
+    <script type="text/javascript">
+      var privacy_notification_enabled = true;
+    </script>
+  </tal:if>
+  <tal:if condition="not:request/features/bugs.private_notification.enabled">
+    <script type="text/javascript">
+      var privacy_notification_enabled = false;
+    </script>
+  </tal:if>
+
+>>>>>>> MERGE-SOURCE
   <script id="base-layout-load-scripts" type="text/javascript">
     LPS.use('node', 'event-delegate', 'lp', 'lp.app.links', 'lp.app.longpoll', function(Y) {
         Y.on('load', function(e) {

=== modified file 'lib/lp/bugs/browser/bugtask.py'
=== modified file 'lib/lp/bugs/doc/bug-nomination.txt'
=== modified file 'lib/lp/bugs/javascript/bugtask_index.js'
=== modified file 'lib/lp/bugs/javascript/tests/test_pre_search.html'
--- lib/lp/bugs/javascript/tests/test_pre_search.html	2011-08-04 00:27:29 +0000
+++ lib/lp/bugs/javascript/tests/test_pre_search.html	2011-08-08 10:56:38 +0000
@@ -1,3 +1,4 @@
+<<<<<<< TREE
 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd";>
 <html>
   <head>
@@ -47,3 +48,54 @@
 <body class="yui3-skin-sam">
 </body>
 </html>
+=======
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd";>
+<html>
+  <head>
+  <title>Branch picker pre-search.</title>
+
+  <!-- YUI and test setup -->
+  <script type="text/javascript"
+          src="../../../../canonical/launchpad/icing/yui/yui/yui.js">
+  </script>
+  <link rel="stylesheet" href="../../../app/javascript/testing/test.css" />
+  <script type="text/javascript"
+          src="../../../app/javascript/testing/testrunner.js"></script>
+
+  <!-- Dependency -->
+  <script type="text/javascript"
+    src="../../../../canonical/launchpad/icing/yui/attribute/attribute.js">
+  </script>
+  <script type="text/javascript"
+    src="../../../../canonical/launchpad/icing/yui/event-custom/event-custom.js">
+  </script>
+  <script type="text/javascript"
+    src="../../../../canonical/launchpad/icing/yui/oop/oop.js">
+  </script>
+  <script type="text/javascript"
+    src="../../../app/javascript/overlay/overlay.js">
+  </script>
+  <script type="text/javascript"
+    src="../../../app/javascript/choiceedit/choiceedit.js">
+  </script>
+  <script type="text/javascript"
+    src="../../../app/javascript/client.js"></script>
+
+  <!-- The module under test -->
+  <script type="text/javascript" src="../bugtask_index.js"></script>
+
+  <!-- The test suite -->
+  <script type="text/javascript" src="test_pre_search.js"></script>
+
+  <!-- Test layout -->
+  <link rel="stylesheet"
+    href="../../../../canonical/launchpad/javascript/test.css" />
+  <style type="text/css">
+    /* CSS classes specific to this test */
+    .unseen { display: none; }
+  </style>
+</head>
+<body class="yui3-skin-sam">
+</body>
+</html>
+>>>>>>> MERGE-SOURCE

=== modified file 'lib/lp/bugs/model/tests/test_bugtask.py'
=== modified file 'lib/lp/bugs/templates/bugcomment-index.pt'
--- lib/lp/bugs/templates/bugcomment-index.pt	2011-08-03 15:27:05 +0000
+++ lib/lp/bugs/templates/bugcomment-index.pt	2011-08-08 10:56:38 +0000
@@ -7,8 +7,29 @@
   i18n:domain="launchpad"
 >
   <body>
-    <metal:block fill-slot="head_epilogue">
-    </metal:block>
+<<<<<<< TREE
+    <metal:block fill-slot="head_epilogue">
+    </metal:block>
+=======
+    <metal:block fill-slot="head_epilogue">
+    <tal:if condition="request/features/bugs.private_notification.enabled">
+    <script>
+        LPS.use('base', 'node', 'lp.app.privacy', function(Y) {
+            Y.on("domready", function () {
+                if (Y.one(document.body).hasClass('private')) {
+                    var config = {
+                        notification_text: 'This comment is on a private bug',
+                        hidden: true
+                    };
+                    Y.lp.app.privacy.setup_privacy_notification(config);
+                    Y.lp.app.privacy.display_privacy_notification(false);
+                }
+            });
+        });
+    </script>
+    </tal:if>
+    </metal:block>
+>>>>>>> MERGE-SOURCE
     <div metal:fill-slot="main" tal:define="comment view/comment">
       <h1 tal:content="view/page_title">Foo doesn't work</h1>
       <tal:comment replace="structure comment/@@+box-expanded-reply">

=== modified file 'lib/lp/bugs/templates/bugtask-index.pt'
--- lib/lp/bugs/templates/bugtask-index.pt	2011-08-04 14:16:05 +0000
+++ lib/lp/bugs/templates/bugtask-index.pt	2011-08-08 10:56:38 +0000
@@ -34,6 +34,7 @@
             });
          });
       </script>
+<<<<<<< TREE
       <tal:if condition="request/features/bugs.private_notification.enabled">
       <script type="text/javascript">
           // We need to set up some bugtask
@@ -45,6 +46,30 @@
             var privacy_notification_enabled = false;
       </script>
       </tal:if>
+=======
+      <tal:if condition="request/features/bugs.private_notification.enabled">
+      <script type="text/javascript">
+        LPS.use('base', 'node', 'oop', 'event', 'lp.bugs.bugtask_index',
+                  'lp.bugs.subscribers',
+                  'lp.code.branchmergeproposal.diff', 'lp.comments.hide',
+                  function(Y) {
+            /*
+             * Display the privacy notification if the bug is private
+             */
+            Y.on("domready", function () {
+                if (Y.one(document.body).hasClass('private')) {
+                    var config = {
+                        notification_text: 'This bug is private',
+                        hidden: true
+                    };
+                    Y.lp.app.privacy.setup_privacy_notification(config);
+                    Y.lp.app.privacy.display_privacy_notification();
+                }
+            });
+        });
+      </script>
+      </tal:if>
+>>>>>>> MERGE-SOURCE
       <style type="text/css">
         /* Align the 'add comment' link to the right of the comment box. */
         #add-comment-form textarea { width: 100%; }

=== modified file 'lib/lp/code/model/tests/test_branchcollection.py'
=== modified file 'lib/lp/code/templates/branch-index.pt'
--- lib/lp/code/templates/branch-index.pt	2011-08-03 15:27:05 +0000
+++ lib/lp/code/templates/branch-index.pt	2011-08-08 10:56:38 +0000
@@ -50,7 +50,28 @@
         }, window);
     });
   "/>
-
+<<<<<<< TREE
+
+=======
+
+  <tal:if condition="request/features/bugs.private_notification.enabled">
+  <script>
+      LPS.use('base', 'node', 'lp.app.privacy', function(Y) {
+          Y.on("domready", function () {
+              if (Y.one(document.body).hasClass('private')) {
+                  var config = {
+                      notification_text: 'This is a private branch',
+                      hidden: true
+                  };
+                  Y.lp.app.privacy.setup_privacy_notification(config);
+                  Y.lp.app.privacy.display_privacy_notification(true);
+              }
+          });
+      });
+  </script>
+  </tal:if>
+
+>>>>>>> MERGE-SOURCE
 </metal:block>
 
 <body>

=== modified file 'lib/lp/code/templates/branchmergeproposal-index.pt'
--- lib/lp/code/templates/branchmergeproposal-index.pt	2011-08-03 15:27:05 +0000
+++ lib/lp/code/templates/branchmergeproposal-index.pt	2011-08-08 10:56:38 +0000
@@ -48,6 +48,22 @@
       padding-bottom: 10px;
     }
   </style>
+  <tal:if condition="request/features/bugs.private_notification.enabled">
+  <script>
+      LPS.use('base', 'node', 'lp.app.privacy', function(Y) {
+          Y.on("domready", function () {
+              if (Y.one(document.body).hasClass('private')) {
+                  var config = {
+                      notification_text: 'This merge proposal is for a private branch',
+                      hidden: true
+                  };
+                  Y.lp.app.privacy.setup_privacy_notification(config);
+                  Y.lp.app.privacy.display_privacy_notification(false);
+              }
+          });
+      });
+  </script>
+  </tal:if>
 </metal:block>
 
 <tal:registering metal:fill-slot="registering">

=== modified file 'lib/lp/registry/browser/distroseries.py'
--- lib/lp/registry/browser/distroseries.py	2011-08-05 17:55:32 +0000
+++ lib/lp/registry/browser/distroseries.py	2011-08-08 10:56:38 +0000
@@ -1109,6 +1109,16 @@
                     packagesets=self.specified_packagesets_filter,
                     changed_by=self.specified_changed_by_filter)
 
+<<<<<<< TREE
+=======
+        differences = getUtility(
+            IDistroSeriesDifferenceSource).getForDistroSeries(
+                self.context, difference_type=self.differences_type,
+                name_filter=self.specified_name_filter,
+                status=status, child_version_higher=child_version_higher,
+                packagesets=self.specified_packagesets_filter,
+                changed_by=self.specified_changed_by_filter)
+>>>>>>> MERGE-SOURCE
         return BatchNavigator(differences, self.request)
 
     @cachedproperty

=== modified file 'lib/lp/registry/browser/tests/test_distroseries.py'
--- lib/lp/registry/browser/tests/test_distroseries.py	2011-08-05 17:55:32 +0000
+++ lib/lp/registry/browser/tests/test_distroseries.py	2011-08-08 10:56:38 +0000
@@ -1619,6 +1619,7 @@
         self.assertContentEqual(
             [diff2, diff1], filtered_view2.cached_differences.batch)
 
+<<<<<<< TREE
     def test_batch_all_packages(self):
         # field.package_type parameter allows to list all the
         # differences.
@@ -1646,13 +1647,35 @@
             derived_series=derived_series,
             source_package_name_str="my-src-package")
         view = create_initialized_view(
+=======
+    def test_batch_all_packages(self):
+        # field.package_type parameter allows to list all the
+        # differences.
+        set_derived_series_ui_feature_flag(self)
+        derived_series, parent_series = self._createChildAndParent()
+        # Create differences of all possible statuses.
+        diffs = []
+        for status in DistroSeriesDifferenceStatus.items:
+            diff = self.factory.makeDistroSeriesDifference(
+                derived_series=derived_series, status=status)
+            diffs.append(diff)
+        all_view = create_initialized_view(
+>>>>>>> MERGE-SOURCE
             derived_series,
             '+localpackagediffs',
+<<<<<<< TREE
             query_string='field.package_type=%s' % 'unexpected')
         view()  # Render the view.
+=======
+            query_string='field.package_type=%s' % ALL)
+>>>>>>> MERGE-SOURCE
 
+<<<<<<< TREE
         self.assertEqual('Invalid option', view.getFieldError('package_type'))
         self.assertContentEqual([], view.cached_differences.batch)
+=======
+        self.assertContentEqual(diffs, all_view.cached_differences.batch)
+>>>>>>> MERGE-SOURCE
 
     def test_batch_blacklisted_differences_with_higher_version(self):
         # field.package_type parameter allows to list only

=== modified file 'lib/lp/registry/model/persontransferjob.py'
--- lib/lp/registry/model/persontransferjob.py	2011-08-04 05:25:00 +0000
+++ lib/lp/registry/model/persontransferjob.py	2011-08-08 10:56:38 +0000
@@ -419,6 +419,7 @@
         to_person_name = self.to_person.name
 
         from canonical.launchpad.scripts import log
+<<<<<<< TREE
         personset = getUtility(IPersonSet)
         if self.metadata.get('delete', False):
             log.debug(
@@ -440,6 +441,29 @@
             log.debug(
                 "%s has merged ~%s into ~%s", self.log_name,
                 from_person_name, to_person_name)
+=======
+        personset = getUtility(IPersonSet)
+        if self.metadata['delete']:
+            log.debug(
+                "%s is about to delete ~%s", self.log_name,
+                from_person_name)
+            personset.delete(
+                from_person=self.from_person,
+                reviewer=self.reviewer)
+            log.debug(
+                "%s has deleted ~%s", self.log_name,
+                from_person_name)
+        else:
+            log.debug(
+                "%s is about to merge ~%s into ~%s", self.log_name,
+                from_person_name, to_person_name)
+            personset.merge(
+                from_person=self.from_person, to_person=self.to_person,
+                reviewer=self.reviewer)
+            log.debug(
+                "%s has merged ~%s into ~%s", self.log_name,
+                from_person_name, to_person_name)
+>>>>>>> MERGE-SOURCE
 
     def __repr__(self):
         return (

=== modified file 'lib/lp/registry/templates/packagesearch-macros.pt'
--- lib/lp/registry/templates/packagesearch-macros.pt	2011-08-04 18:33:35 +0000
+++ lib/lp/registry/templates/packagesearch-macros.pt	2011-08-08 10:56:38 +0000
@@ -63,7 +63,11 @@
     class="distroseries-localdiff-search-filter"
     action="" method="GET">
     <div style="float:left; margin-right:1px;">
+<<<<<<< TREE
       <label for="field.name_filter">Show packages or packagesets named:</label>
+=======
+      <label for="field.name_filter">Show packages with names or packagesets matching:</label>
+>>>>>>> MERGE-SOURCE
     </div>
     <div style="float:left;"
       tal:define="widget nocall:view/widgets/package_type;

=== modified file 'lib/lp/registry/tests/test_dsp_vocabularies.py'
--- lib/lp/registry/tests/test_dsp_vocabularies.py	2011-08-03 05:55:54 +0000
+++ lib/lp/registry/tests/test_dsp_vocabularies.py	2011-08-08 10:56:38 +0000
@@ -20,6 +20,7 @@
             self.factory.makeDistribution())
         self.assertProvides(vocabulary, IHugeVocabulary)
 
+<<<<<<< TREE
     def test_init_IDistribution(self):
         # When the context is adaptable to IDistribution, it also provides
         # the distribution.
@@ -156,6 +157,142 @@
         self.assertEqual(expected_title, term.title)
 
     def test_toTerm_built_single_binary_title(self):
+=======
+    def test_init_IDistribution(self):
+        # When the context is adaptable to IDistribution, it also provides
+        # the distribution.
+        dsp = self.factory.makeDistributionSourcePackage(
+            sourcepackagename='foo')
+        vocabulary = DistributionSourcePackageVocabulary(dsp)
+        self.assertEqual(dsp, vocabulary.context)
+        self.assertEqual(dsp.distribution, vocabulary.distribution)
+
+    def test_init_dsp_bugtask(self):
+        # A dsp bugtask can be the context
+        dsp = self.factory.makeDistributionSourcePackage(
+            sourcepackagename='foo')
+        bugtask = self.factory.makeBugTask(target=dsp)
+        vocabulary = DistributionSourcePackageVocabulary(bugtask)
+        self.assertEqual(bugtask, vocabulary.context)
+        self.assertEqual(dsp.distribution, vocabulary.distribution)
+
+    def test_init_dsp_question(self):
+        # A dsp bugtask can be the context
+        dsp = self.factory.makeDistributionSourcePackage(
+            sourcepackagename='foo')
+        question = self.factory.makeQuestion(
+            target=dsp, owner=dsp.distribution.owner)
+        vocabulary = DistributionSourcePackageVocabulary(question)
+        self.assertEqual(question, vocabulary.context)
+        self.assertEqual(dsp.distribution, vocabulary.distribution)
+
+    def test_init_no_distribution(self):
+        # The distribution is None if the context cannot be adapted to a
+        # distribution.
+        project = self.factory.makeProduct()
+        vocabulary = DistributionSourcePackageVocabulary(project)
+        self.assertEqual(project, vocabulary.context)
+        self.assertEqual(None, vocabulary.distribution)
+
+    def test_getDistributionAndPackageName_distro_and_package(self):
+        # getDistributionAndPackageName() returns a tuple of distribution
+        # and package name when the text contains both.
+        new_distro = self.factory.makeDistribution(name='fnord')
+        vocabulary = DistributionSourcePackageVocabulary(None)
+        distribution, package_name = vocabulary.getDistributionAndPackageName(
+            'fnord/pting')
+        self.assertEqual(new_distro, distribution)
+        self.assertEqual('pting', package_name)
+
+    def test_getDistributionAndPackageName_default_distro_and_package(self):
+        # getDistributionAndPackageName() returns a tuple of the default
+        # distribution and package name when the text is just a package name.
+        default_distro = self.factory.makeDistribution(name='fnord')
+        vocabulary = DistributionSourcePackageVocabulary(default_distro)
+        distribution, package_name = vocabulary.getDistributionAndPackageName(
+            'pting')
+        self.assertEqual(default_distro, distribution)
+        self.assertEqual('pting', package_name)
+
+    def test_getDistributionAndPackageName_bad_distro_and_package(self):
+        # getDistributionAndPackageName() returns a tuple of the default
+        # distribution and package name when the distro in the text cannot
+        # be matched to a real distro.
+        default_distro = self.factory.makeDistribution(name='fnord')
+        vocabulary = DistributionSourcePackageVocabulary(default_distro)
+        distribution, package_name = vocabulary.getDistributionAndPackageName(
+            'misspelled/pting')
+        self.assertEqual(default_distro, distribution)
+        self.assertEqual('pting', package_name)
+
+    def test_contains_true(self):
+        # The vocabulary contains DSPs that have SPPH in the distro.
+        spph = self.factory.makeSourcePackagePublishingHistory()
+        dsp = spph.sourcepackagerelease.distrosourcepackage
+        vocabulary = DistributionSourcePackageVocabulary(dsp)
+        self.assertTrue(dsp in vocabulary)
+
+    def test_contains_false(self):
+        # The vocabulary does not contain DSPs without SPPH.
+        spn = self.factory.makeSourcePackageName(name='foo')
+        dsp = self.factory.makeDistributionSourcePackage(
+            sourcepackagename=spn)
+        vocabulary = DistributionSourcePackageVocabulary(dsp)
+        self.assertFalse(dsp in vocabulary)
+
+    def test_toTerm_raises_error(self):
+        # An error is raised for DSP/SPNs without publishing history.
+        dsp = self.factory.makeDistributionSourcePackage(
+            sourcepackagename='foo')
+        vocabulary = DistributionSourcePackageVocabulary(dsp.distribution)
+        self.assertRaises(LookupError, vocabulary.toTerm, dsp.name)
+
+    def test_toTerm_none_raises_error(self):
+        # An error is raised for SPN does not exist.
+        vocabulary = DistributionSourcePackageVocabulary(None)
+        self.assertRaises(LookupError, vocabulary.toTerm, 'non-existant')
+
+    def test_toTerm_spn_and_default_distribution(self):
+        # The vocabulary's distribution is used when only a SPN is passed.
+        spph = self.factory.makeSourcePackagePublishingHistory()
+        dsp = spph.sourcepackagerelease.distrosourcepackage
+        vocabulary = DistributionSourcePackageVocabulary(dsp.distribution)
+        term = vocabulary.toTerm(dsp.sourcepackagename)
+        expected_token = '%s/%s' % (dsp.distribution.name, dsp.name)
+        self.assertEqual(expected_token, term.token)
+        self.assertEqual(dsp.sourcepackagename, term.value)
+
+    def test_toTerm_spn_and_distribution(self):
+        # The distribution is used with the spn if it is passed.
+        spph = self.factory.makeSourcePackagePublishingHistory()
+        dsp = spph.sourcepackagerelease.distrosourcepackage
+        vocabulary = DistributionSourcePackageVocabulary(None)
+        term = vocabulary.toTerm(dsp.sourcepackagename, dsp.distribution)
+        expected_token = '%s/%s' % (dsp.distribution.name, dsp.name)
+        self.assertEqual(expected_token, term.token)
+        self.assertEqual(dsp.sourcepackagename, term.value)
+
+    def test_toTerm_dsp(self):
+        # The DSP's distribution is used when a DSP is passed.
+        spph = self.factory.makeSourcePackagePublishingHistory()
+        dsp = spph.sourcepackagerelease.distrosourcepackage
+        vocabulary = DistributionSourcePackageVocabulary(dsp)
+        term = vocabulary.toTerm(dsp)
+        expected_token = '%s/%s' % (dsp.distribution.name, dsp.name)
+        self.assertEqual(expected_token, term.token)
+        self.assertEqual(dsp.sourcepackagename, term.value)
+
+    def test_toTerm_unbuilt_title(self):
+        # The DSP's distribution is used with the spn if it is passed.
+        # Published, but unbuilt packages state the case in the term title.
+        spph = self.factory.makeSourcePackagePublishingHistory()
+        dsp = spph.sourcepackagerelease.distrosourcepackage
+        vocabulary = DistributionSourcePackageVocabulary(dsp)
+        term = vocabulary.toTerm(dsp)
+        self.assertEqual('Not yet built.', term.title)
+
+    def test_toTerm_built_single_binary_title(self):
+>>>>>>> MERGE-SOURCE
         # The binary package name appears in the term's value.
         bpph = self.factory.makeBinaryPackagePublishingHistory()
         spr = bpph.binarypackagerelease.build.source_package_release
@@ -164,10 +301,14 @@
             distribution=bpph.distroseries.distribution)
         vocabulary = DistributionSourcePackageVocabulary(dsp.distribution)
         term = vocabulary.toTerm(spr.sourcepackagename)
+<<<<<<< TREE
         expected_title = '%s/%s %s' % (
             dsp.distribution.name, spr.sourcepackagename.name,
             bpph.binary_package_name)
         self.assertEqual(expected_title, term.title)
+=======
+        self.assertEqual(bpph.binary_package_name, term.title)
+>>>>>>> MERGE-SOURCE
 
     def test_toTerm_built_multiple_binary_title(self):
         # All of the binary package names appear in the term's value.
@@ -186,6 +327,7 @@
         dsp = spr.distrosourcepackage
         vocabulary = DistributionSourcePackageVocabulary(dsp.distribution)
         term = vocabulary.toTerm(spr.sourcepackagename)
+<<<<<<< TREE
         expected_title = '%s/%s %s' % (
             dsp.distribution.name, spph.source_package_name,
             ', '.join(expected_names))
@@ -216,6 +358,35 @@
         vocabulary = DistributionSourcePackageVocabulary(dsp.name)
         results = vocabulary.searchForTerms(dsp.name)
         self.assertIs(0, results.count())
+=======
+        self.assertEqual(', '.join(expected_names), term.title)
+
+    def test_getTermByToken_error(self):
+        # An error is raised if the token does not match a published DSP.
+        dsp = self.factory.makeDistributionSourcePackage(
+            sourcepackagename='foo')
+        vocabulary = DistributionSourcePackageVocabulary(dsp.distribution)
+        token = '%s/%s' % (dsp.distribution.name, dsp.name)
+        self.assertRaises(LookupError, vocabulary.getTermByToken, token)
+
+    def test_getTermByToken_token(self):
+        # The term is return if it matches a published DSP.
+        spph = self.factory.makeSourcePackagePublishingHistory()
+        dsp = spph.sourcepackagerelease.distrosourcepackage
+        vocabulary = DistributionSourcePackageVocabulary(dsp.distribution)
+        token = '%s/%s' % (dsp.distribution.name, dsp.name)
+        term = vocabulary.getTermByToken(token)
+        self.assertEqual(dsp.sourcepackagename, term.value)
+
+    def test_searchForTerms_without_distribution(self):
+        # An empty result set is return if the vocabulary has no distribution
+        # and the search does not provide distribution information.
+        spph = self.factory.makeSourcePackagePublishingHistory()
+        dsp = spph.sourcepackagerelease.distrosourcepackage
+        vocabulary = DistributionSourcePackageVocabulary(dsp.name)
+        results = vocabulary.searchForTerms(dsp.name)
+        self.assertIs(0, results.count())
+>>>>>>> MERGE-SOURCE
 
     def test_searchForTerms_None(self):
         # Searching for nothing gets you that.

=== modified file 'lib/lp/registry/tests/test_person.py'
--- lib/lp/registry/tests/test_person.py	2011-08-03 11:00:11 +0000
+++ lib/lp/registry/tests/test_person.py	2011-08-08 10:56:38 +0000
@@ -757,6 +757,7 @@
         account.openid_identifier = openid_identifier
         return account
 
+<<<<<<< TREE
     def test_delete_no_notifications(self):
         team = self.factory.makeTeam()
         owner = team.teamowner
@@ -769,6 +770,19 @@
         notifications = notification_set.getNotificationsToSend()
         self.assertEqual(0, notifications.count())
 
+=======
+    def test_delete_no_notifications(self):
+        team = self.factory.makeTeam()
+        owner = team.teamowner
+        transaction.commit()
+        reconnect_stores('IPersonMergeJobSource')
+        team = reload_object(team)
+        owner = reload_object(owner)
+        self.person_set.delete(team, owner)
+        notifications = getUtility(IPersonNotificationSet).getNotificationsToSend()
+        self.assertEqual(0, notifications.count())
+
+>>>>>>> MERGE-SOURCE
     def test_openid_identifiers(self):
         # Verify that OpenId Identifiers are merged.
         duplicate = self.factory.makePerson()

=== modified file 'lib/lp/registry/vocabularies.py'
--- lib/lp/registry/vocabularies.py	2011-08-02 23:40:08 +0000
+++ lib/lp/registry/vocabularies.py	2011-08-08 10:56:38 +0000
@@ -2079,6 +2079,7 @@
 
     def __init__(self, context):
         self.context = context
+<<<<<<< TREE
         # Avoid circular import issues.
         from lp.answers.interfaces.question import IQuestion
         if IReference.providedBy(context):
@@ -2091,6 +2092,18 @@
             self.distribution = IDistribution(target)
         except TypeError:
             self.distribution = None
+=======
+        # Avoid circular import issues.
+        from lp.answers.interfaces.question import IQuestion
+        if IBugTask.providedBy(context) or IQuestion.providedBy(context):
+            target = context.target
+        else:
+            target = context
+        try:
+            self.distribution = IDistribution(target)
+        except TypeError:
+            self.distribution = None
+>>>>>>> MERGE-SOURCE
 
     def __contains__(self, spn_or_dsp):
         try:
@@ -2118,6 +2131,7 @@
 
     def toTerm(self, spn_or_dsp, distribution=None):
         """See `IVocabulary`."""
+<<<<<<< TREE
         dsp = None
         if IDistributionSourcePackage.providedBy(spn_or_dsp):
             dsp = spn_or_dsp
@@ -2128,7 +2142,19 @@
                 dsp = distribution.getSourcePackage(spn_or_dsp)
         try:
             token = '%s/%s' % (dsp.distribution.name, dsp.name)
+=======
+        dsp = None
+        if IDistributionSourcePackage.providedBy(spn_or_dsp):
+            dsp = spn_or_dsp
+            distribution = spn_or_dsp.distribution
+        else:
+            distribution = distribution or self.distribution
+            if distribution is not None and spn_or_dsp is not None:
+                dsp = distribution.getSourcePackage(spn_or_dsp)
+        try:
+>>>>>>> MERGE-SOURCE
             binaries = dsp.publishing_history[0].getBuiltBinaries()
+<<<<<<< TREE
             binary_names = [binary.binary_package_name for binary in binaries]
             if binary_names != []:
                 summary = ', '.join(binary_names)
@@ -2139,6 +2165,18 @@
         except (IndexError, AttributeError):
             # Either the DSP was None or there is no publishing history.
             raise LookupError(distribution, spn_or_dsp)
+=======
+            binary_names = [binary.binary_package_name for binary in binaries]
+            if binary_names != []:
+                summary = ', '.join(binary_names)
+            else:
+                summary = 'Not yet built.'
+            token = '%s/%s' % (dsp.distribution.name, dsp.name)
+            return SimpleTerm(dsp.sourcepackagename, token, summary)
+        except (IndexError, AttributeError):
+            # Either the DSP was None or there is no publishing history.
+            raise LookupError(distribution, spn_or_dsp)
+>>>>>>> MERGE-SOURCE
 
     def getTerm(self, spn_or_dsp):
         """See `IBaseVocabulary`."""

=== modified file 'lib/lp/scripts/garbo.py'
=== modified file 'lib/lp/services/features/flags.py'
=== modified file 'lib/lp/soyuz/model/distroseriesdifferencejob.py'
=== modified file 'lib/lp/soyuz/tests/test_distroseriesdifferencejob.py'
=== modified file 'lib/lp/testing/factory.py'
=== modified file 'lib/lp/translations/tests/test_translationimportqueue.py'
=== modified file 'versions.cfg'