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