Merge lp:~jtv/launchpad/cp-bug-403992 into lp:launchpad/db-devel

Proposed by Jeroen T. Vermeulen
Status: Rejected
Rejected by: Jeroen T. Vermeulen
Proposed branch: lp:~jtv/launchpad/cp-bug-403992
Merge into: lp:launchpad/db-devel
Diff against target: None lines
To merge this branch: bzr merge lp:~jtv/launchpad/cp-bug-403992
Reviewer Review Type Date Requested Status
Canonical Launchpad Engineering Pending
Review via email: mp+9689@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

= Bug 403992 =

I'm hoping to get this branch cherry-picked.

Message-sharing migration is failing. This is the command that broke on
us:

    ./scripts/rosetta/message-sharing-merge.py -vvv -P elisa

The problem is with some caching the script does. It keeps a cache of
TranslationMessages for the representative POTMsgSet it's merging
subordinate POTMsgSets into. It uses this cache to detect when moving
a current or imported TranslationMessage from a subordinate POTMsgSet to
the representative POTMsgSet would conflict with an existing current or
imported TranslationMessage.

Unfortunately the script fails to update this cache with the additions
to the representative POTMsgSet as it moves TranslationMessages over.
Thus a conflict could go undetected and violate a unique constraint on
the database.

This branch fixes the problem minimally by using the non-caching method
to detect clashes, rather than the broken cache. A separate branch,
lp:~jtv/launchpad/bug-403992 also culls the dead code and its unit
tests.

There are no test changes here. Basic functionality is retained, and I
see no easy way for this exact problem to come back now that the cache
is no longer in use.

Jeroen

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/canonical/config/schema-lazr.conf'
--- lib/canonical/config/schema-lazr.conf 2009-07-29 09:46:46 +0000
+++ lib/canonical/config/schema-lazr.conf 2009-08-05 02:18:13 +0000
@@ -584,6 +584,16 @@
584# in refcounts and memory.584# in refcounts and memory.
585references_leak_log: /tmp/references-leak.log585references_leak_log: /tmp/references-leak.log
586586
587[diff]
588# The maximum size in bytes to read from the librarian to make available in
589# the web UI. 512k == 524288 bytes.
590# datatype: integer
591max_read_size: 524288
592
593# The maximum number of lines to format using the format_diff tal formatter.
594max_format_lines: 5000
595
596
587[distributionmirrorprober]597[distributionmirrorprober]
588# The database user which will be used by this process.598# The database user which will be used by this process.
589# datatype: string599# datatype: string
590600
=== modified file 'lib/canonical/launchpad/emailtemplates/branch-merge-proposal-created.txt'
--- lib/canonical/launchpad/emailtemplates/branch-merge-proposal-created.txt 2008-11-26 07:40:34 +0000
+++ lib/canonical/launchpad/emailtemplates/branch-merge-proposal-created.txt 2009-08-04 09:15:32 +0000
@@ -1,6 +1,6 @@
1%(proposal_registrant)s has proposed merging %(source_branch)s into %(target_branch)s.1%(proposal_registrant)s has proposed merging %(source_branch)s into %(target_branch)s.
22
3%(reviews)s%(gap)s%(comment)s3%(reviews)s%(gap)s%(comment)s%(diff_cutoff_warning)s
4-- 4--
5%(proposal_url)s5%(proposal_url)s
6%(reason)s%(edit_subscription)s6%(reason)s%(edit_subscription)s
77
=== modified file 'lib/canonical/launchpad/emailtemplates/review-requested.txt'
--- lib/canonical/launchpad/emailtemplates/review-requested.txt 2009-02-15 23:44:45 +0000
+++ lib/canonical/launchpad/emailtemplates/review-requested.txt 2009-08-04 09:15:32 +0000
@@ -1,7 +1,7 @@
1You have been requested to review the proposed merge of %(source_branch)s into %(target_branch)s.1You have been requested to review the proposed merge of %(source_branch)s into %(target_branch)s.
22
3%(comment)s3%(comment)s%(diff_cutoff_warning)s
44
5--5--
6%(proposal_url)s6%(proposal_url)s
7%(reason)s%(edit_subscription)s7%(reason)s%(edit_subscription)s
88
=== modified file 'lib/canonical/launchpad/interfaces/_schema_circular_imports.py'
--- lib/canonical/launchpad/interfaces/_schema_circular_imports.py 2009-08-04 05:02:41 +0000
+++ lib/canonical/launchpad/interfaces/_schema_circular_imports.py 2009-08-05 07:35:57 +0000
@@ -75,6 +75,11 @@
75IBranch['linkSpecification'].queryTaggedValue(75IBranch['linkSpecification'].queryTaggedValue(
76 LAZR_WEBSERVICE_EXPORTED)['params']['spec'].schema= ISpecification76 LAZR_WEBSERVICE_EXPORTED)['params']['spec'].schema= ISpecification
77IBranch['product'].schema = IProduct77IBranch['product'].schema = IProduct
78IBranch['setTarget'].queryTaggedValue(
79 LAZR_WEBSERVICE_EXPORTED)['params']['project'].schema= IProduct
80IBranch['setTarget'].queryTaggedValue(
81 LAZR_WEBSERVICE_EXPORTED)['params']['source_package'].schema= \
82 ISourcePackage
78IBranch['spec_links'].value_type.schema = ISpecificationBranch83IBranch['spec_links'].value_type.schema = ISpecificationBranch
79IBranch['subscribe'].queryTaggedValue(84IBranch['subscribe'].queryTaggedValue(
80 LAZR_WEBSERVICE_EXPORTED)['return_type'].schema = IBranchSubscription85 LAZR_WEBSERVICE_EXPORTED)['return_type'].schema = IBranchSubscription
8186
=== modified file 'lib/canonical/launchpad/javascript/bugs/bugtask-index.js'
--- lib/canonical/launchpad/javascript/bugs/bugtask-index.js 2009-08-04 12:26:40 +0000
+++ lib/canonical/launchpad/javascript/bugs/bugtask-index.js 2009-08-05 09:33:08 +0000
@@ -1319,6 +1319,155 @@
1319 }1319 }
1320};1320};
13211321
1322/**
1323 * Set up the "me too" selection.
1324 *
1325 * Called once, on load, to initialize the page. Call this function if
1326 * the "me too" information is displayed on a bug page and the user is
1327 * logged in.
1328 *
1329 * @method setup_me_too
1330 */
1331bugs.setup_me_too = function(user_is_affected) {
1332 var me_too_content = Y.get('#affectsmetoo');
1333 var me_too_edit = new MeTooChoiceSource({
1334 contentBox: me_too_content, value: user_is_affected,
1335 elementToFlash: me_too_content
1336 });
1337 me_too_edit.render();
1338};
1339
1340/**
1341 * This class is a derivative of ChoiceSource that handles the
1342 * specifics of editing "me too" option.
1343 *
1344 * @class MeTooChoiceSource
1345 * @extends ChoiceSource
1346 * @constructor
1347 */
1348function MeTooChoiceSource() {
1349 MeTooChoiceSource.superclass.constructor.apply(this, arguments);
1350}
1351
1352MeTooChoiceSource.NAME = 'metoocs';
1353MeTooChoiceSource.NS = 'metoocs';
1354
1355MeTooChoiceSource.HTML_PARSER = {
1356 flame_icon: ".dynamic img[src$=/@@/flame-icon]"
1357};
1358
1359MeTooChoiceSource.ATTRS = {
1360 /**
1361 * The title is always the same, so bake it in here.
1362 *
1363 * @attribute title
1364 * @type String
1365 */
1366 title: {
1367 value: 'Does this bug affect you?'
1368 },
1369
1370 /**
1371 * The items are always the same, so bake them in here.
1372 *
1373 * @attribute items
1374 * @type Array
1375 */
1376 items: {
1377 value: [
1378 { name: 'Yes, it affects me', value: true,
1379 source_name: 'This bug affects me too',
1380 disabled: false },
1381 { name: "No, it doesn't affect me", value: false,
1382 source_name: "This bug doesn't affect me",
1383 disabled: false }
1384 ]
1385 },
1386
1387 /**
1388 * Y.Node containing a flame icon, displayed when the user is
1389 * affected by the current bug. Should be automatically calculated
1390 * by HTML_PARSER.
1391 *
1392 * Setter function returns Y.get(parameter) so that you can pass
1393 * either a Node (as expected) or a selector.
1394 *
1395 * @attribute value_location
1396 * @type Node
1397 */
1398 flame_icon: {
1399 value: null,
1400 set: function(v) {
1401 return Y.get(v);
1402 }
1403 }
1404};
1405
1406// Put this in the bugs namespace so it can be accessed for testing.
1407bugs._MeTooChoiceSource = MeTooChoiceSource;
1408
1409Y.extend(MeTooChoiceSource, Y.ChoiceSource, {
1410 initializer: function() {
1411 var widget = this;
1412 this.error_handler = new LP.client.ErrorHandler();
1413 this.error_handler.clearProgressUI = function() {
1414 widget._uiClearWaiting();
1415 };
1416 this.error_handler.showError = function(error_msg) {
1417 widget.showError(error_msg);
1418 };
1419 },
1420
1421 showError: function(err) {
1422 display_error(null, err);
1423 },
1424
1425 syncUI: function() {
1426 MeTooChoiceSource.superclass.syncUI.apply(this, arguments);
1427 // Show the flame icon if the user is affected by this bug.
1428 if (this.get('value')) {
1429 this.get('flame_icon').removeClass('unseen');
1430 } else {
1431 this.get('flame_icon').addClass('unseen');
1432 }
1433 },
1434
1435 render: function() {
1436 MeTooChoiceSource.superclass.render.apply(this, arguments);
1437 // Force the ChoiceSource to be rendered inline.
1438 this.get('boundingBox').setStyle('display', 'inline');
1439 // Hide the static content and show the dynamic content.
1440 this.get('contentBox').query('.static').addClass('unseen');
1441 this.get('contentBox').query('.dynamic').removeClass('unseen');
1442 },
1443
1444 _saveData: function() {
1445 // Set the widget to the 'waiting' state.
1446 this._uiSetWaiting();
1447
1448 var value = this.getInput();
1449 var client = new LP.client.Launchpad();
1450 var widget = this;
1451
1452 var config = {
1453 on: {
1454 success: function(entry) {
1455 widget._uiClearWaiting();
1456 MeTooChoiceSource.superclass._saveData.call(
1457 widget, value);
1458 },
1459 failure: this.error_handler.getFailureHandler()
1460 },
1461 parameters: {
1462 affected: value
1463 }
1464 };
1465
1466 client.named_post(
1467 LP.client.cache.bug.self_link, 'markUserAffected', config);
1468 }
1469});
1470
1322/*1471/*
1323 * Check if the current user can unsubscribe the person1472 * Check if the current user can unsubscribe the person
1324 * being subscribed.1473 * being subscribed.
@@ -1503,6 +1652,6 @@
1503}1652}
15041653
1505}, '0.1', {requires: ['base', 'oop', 'node', 'event', 'io-base', 'substitute',1654}, '0.1', {requires: ['base', 'oop', 'node', 'event', 'io-base', 'substitute',
1506 'widget-position-ext', 'lazr.formoverlay', 'lazr.anim', 1655 'widget-position-ext', 'lazr.formoverlay', 'lazr.anim',
1507 'lazr.base', 'lazr.overlay', 'lazr.choiceedit',1656 'lazr.base', 'lazr.overlay', 'lazr.choiceedit',
1508 'lp.picker', 'lp.client.plugins', 'lp.subscriber']});1657 'lp.picker', 'lp.client.plugins', 'lp.subscriber']});
15091658
=== added file 'lib/canonical/launchpad/javascript/bugs/tests/test_me_too.html'
--- lib/canonical/launchpad/javascript/bugs/tests/test_me_too.html 1970-01-01 00:00:00 +0000
+++ lib/canonical/launchpad/javascript/bugs/tests/test_me_too.html 2009-08-04 16:48:41 +0000
@@ -0,0 +1,34 @@
1<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
2<html>
3 <head>
4 <title>Status Editor</title>
5
6 <!-- YUI 3.0 Setup -->
7 <script type="text/javascript" src="../../../icing/yui/3.0.0pr2/build/yui/yui.js"></script>
8 <link rel="stylesheet" href="../../../icing/yui/3.0.0pr2/build/cssreset/reset.css"/>
9 <link rel="stylesheet" href="../../../icing/yui/3.0.0pr2/build/cssfonts/fonts.css"/>
10 <link rel="stylesheet" href="../../../icing/yui/3.0.0pr2/build/cssbase/base.css"/>
11
12 <!-- Dependency -->
13 <script type="text/javascript" src="../../../icing/lazr/build/lazr.js"></script>
14 <script type="text/javascript" src="../../../icing/lazr/build/overlay/overlay.js"></script>
15 <script type="text/javascript" src="../../../icing/lazr/build/choiceedit/choiceedit.js"></script>
16 <script type="text/javascript" src="../../../javascript/client/client.js"></script>
17
18 <!-- The module under test -->
19 <script type="text/javascript" src="../bugtask-index.js"></script>
20
21 <!-- The test suite -->
22 <script type="text/javascript" src="test_me_too.js"></script>
23
24 <!-- Test layout -->
25 <link rel="stylesheet" href="../../test.css" />
26 <style type="text/css">
27 /* CSS classes specific to this test */
28 .unseen { display: none; }
29 </style>
30</head>
31<body class="yui-skin-sam">
32 <div id="log"></div>
33</body>
34</html>
035
=== added file 'lib/canonical/launchpad/javascript/bugs/tests/test_me_too.js'
--- lib/canonical/launchpad/javascript/bugs/tests/test_me_too.js 1970-01-01 00:00:00 +0000
+++ lib/canonical/launchpad/javascript/bugs/tests/test_me_too.js 2009-08-04 16:19:27 +0000
@@ -0,0 +1,223 @@
1/* Copyright (c) 2008, Canonical Ltd. All rights reserved. */
2
3YUI({
4 base: '../../../icing/yui/current/build/',
5 filter: 'raw',
6 combine: false
7 }).use('event', 'bugs.bugtask_index', 'node', 'yuitest', 'widget-stack', 'console',
8 function(Y) {
9
10// Local aliases
11var Assert = Y.Assert,
12 ArrayAssert = Y.ArrayAssert;
13
14/*
15 * A wrapper for the Y.Event.simulate() function. The wrapper accepts
16 * CSS selectors and Node instances instead of raw nodes.
17 */
18function simulate(widget, selector, evtype, options) {
19 var rawnode = Y.Node.getDOMNode(widget.query(selector));
20 Y.Event.simulate(rawnode, evtype, options);
21}
22
23/* Helper function to clean up a dynamically added widget instance. */
24function cleanup_widget(widget) {
25 // Nuke the boundingBox, but only if we've touched the DOM.
26 if (widget.get('rendered')) {
27 var bb = widget.get('boundingBox');
28 if (bb.get('parentNode')) {
29 bb.get('parentNode').removeChild(bb);
30 }
31 }
32 // Kill the widget itself.
33 widget.destroy();
34}
35
36var suite = new Y.Test.Suite("Bugtask Me-Too Choice Edit Tests");
37
38suite.add(new Y.Test.Case({
39
40 name: 'me_too_choice_edit_basics',
41
42 setUp: function() {
43 // Monkeypatch LP.client to avoid network traffic and to make
44 // some things work as expected.
45 LP.client.Launchpad.prototype.named_post =
46 function(url, func, config) {
47 config.on.success();
48 };
49 LP.client.cache.bug = {
50 self_link: "http://bugs.example.com/bugs/1234"
51 };
52 // add the in-page HTML
53 var inpage = Y.Node.create([
54 '<span id="affectsmetoo">',
55 ' <span class="static">',
56 ' <img src="https://bugs.edge.launchpad.net/@@/flame-icon" alt="" />',
57 ' This bug affects me too',
58 ' <a href="+affectsmetoo">',
59 ' <img class="editicon" alt="Edit"',
60 ' src="https://bugs.edge.launchpad.net/@@/edit" />',
61 ' </a>',
62 ' </span>',
63 ' <span class="dynamic unseen">',
64 ' <img class="editicon" alt="Edit"',
65 ' src="https://bugs.edge.launchpad.net/@@/edit" />',
66 ' <a href="+affectsmetoo" class="js-action"',
67 ' ><span class="value">Does this bug affect you?</span></a>',
68 ' <img src="https://bugs.edge.launchpad.net/@@/flame-icon" alt=""/>',
69 ' </span>',
70 '</span>'].join(''));
71 Y.get("body").appendChild(inpage);
72 var me_too_content = Y.get('#affectsmetoo');
73 this.config = {
74 contentBox: me_too_content, value: null,
75 elementToFlash: me_too_content
76 };
77 this.choice_edit = new Y.bugs._MeTooChoiceSource(this.config);
78 this.choice_edit.render();
79 },
80
81 tearDown: function() {
82 if (this.choice_edit._choice_list) {
83 cleanup_widget(this.choice_edit._choice_list);
84 }
85 var status = Y.get("document").query("#affectsmetoo");
86 if (status) {
87 status.get("parentNode").removeChild(status);
88 }
89 },
90
91 /**
92 * The choice edit should be displayed inline.
93 */
94 test_is_inline: function() {
95 var display = this.choice_edit.get('boundingBox').getStyle('display');
96 Assert.areEqual(
97 display, 'inline', "Not displayed inline, display is: " + display);
98 },
99
100 /**
101 * The .static area should be hidden by adding the "unseen" class.
102 */
103 test_hide_static: function() {
104 var static_area = this.choice_edit.get('contentBox').query('.static');
105 Assert.isTrue(
106 static_area.hasClass('unseen'), "Static area is not hidden.");
107 },
108
109 /**
110 * The .dynamic area should be shown by removing the "unseen" class.
111 */
112 test_hide_dynamic: function() {
113 var dynamic_area = this.choice_edit.get('contentBox').query('.dynamic');
114 Assert.isFalse(
115 dynamic_area.hasClass('unseen'), "Dynamic area is hidden.");
116 },
117
118 /**
119 * The flame icon should be hidden initially.
120 */
121 test_flame_hidden_initially: function() {
122 var flame_icon = this.choice_edit.get('flame_icon');
123 Assert.isTrue(flame_icon.hasClass('unseen'), "Flame is not hidden.");
124 },
125
126 /**
127 * The flame icon should be hidden when the user has made a
128 * negative choice (i.e. "Does not affect me").
129 */
130 test_flame_hidden_with_negative_choice: function() {
131 simulate(this.choice_edit.get('boundingBox'), '.value', 'mousedown');
132 simulate(this.choice_edit._choice_list.get('boundingBox'),
133 'li a[href$=false]', 'click');
134 var flame_icon = this.choice_edit.get('flame_icon');
135 Assert.isTrue(flame_icon.hasClass('unseen'), "Flame is not hidden.");
136 },
137
138 /**
139 * The flame icon should be shown when the user has made a
140 * positive choice (i.e. "Affects me too").
141 */
142 test_flame_hidden_with_positive_choice: function() {
143 simulate(this.choice_edit.get('boundingBox'), '.value', 'mousedown');
144 simulate(this.choice_edit._choice_list.get('boundingBox'),
145 'li a[href$=true]', 'click');
146 var flame_icon = this.choice_edit.get('flame_icon');
147 Assert.isFalse(flame_icon.hasClass('unseen'), "Flame is hidden.");
148 },
149
150 /**
151 * The UI should be in a waiting state while the save process is
152 * executing and return to a non-waiting state once it has
153 * finished.
154 */
155 test_ui_waiting_for_success: function() {
156 this.do_test_ui_waiting('success');
157 },
158
159 /**
160 * The UI should be in a waiting state while the save process is
161 * executing and return to a non-waiting state even if the process
162 * fails.
163 */
164 test_ui_waiting_for_failure: function() {
165 this.do_test_ui_waiting('failure');
166 },
167
168 /**
169 * Helper function that does the leg work for the
170 * test_ui_waiting_* methods.
171 */
172 do_test_ui_waiting: function(callback) {
173 var edit_icon = this.choice_edit.get('editicon');
174 // The spinner should not be displayed at first.
175 Assert.isNull(
176 edit_icon.get('src').match(/\/spinner$/),
177 "The edit icon is displaying a spinner at rest.");
178 // The spinner should not be displayed after opening the
179 // choice list.
180 simulate(this.choice_edit.get('boundingBox'), '.value', 'mousedown');
181 Assert.isNull(
182 edit_icon.get('src').match(/\/spinner$/),
183 "The edit icon is displaying a spinner after opening the choice list.");
184 // The spinner should be visible during the interval between a
185 // choice being made and a response coming back from Launchpad
186 // that the choice has been saved.
187 var edit_icon_src_during_save;
188 // Patch the named_post method to simulate success or failure,
189 // as determined by the callback argument. We cannot make
190 // assertions in this method because exceptions are swallowed
191 // somewhere. Instead, we save something testable to a local
192 // var.
193 LP.client.Launchpad.prototype.named_post =
194 function(url, func, config) {
195 edit_icon_src_during_save = edit_icon.get('src');
196 config.on[callback]();
197 };
198 simulate(this.choice_edit._choice_list.get('boundingBox'),
199 'li a[href$=true]', 'click');
200 Assert.isNotNull(
201 edit_icon_src_during_save.match(/\/spinner$/),
202 "The edit icon is not displaying a spinner during save.");
203 // The spinner should not be displayed once a choice has been
204 // saved.
205 Assert.isNull(
206 edit_icon.get('src').match(/\/spinner$/),
207 "The edit icon is displaying a spinner once the choice has been made.");
208 }
209
210}));
211
212Y.Test.Runner.add(suite);
213
214var yconsole = new Y.Console({
215 newestOnTop: false
216});
217yconsole.render('#log');
218
219Y.on('domready', function() {
220 Y.Test.Runner.run();
221});
222
223});
0224
=== modified file 'lib/canonical/launchpad/webapp/adapter.py'
--- lib/canonical/launchpad/webapp/adapter.py 2009-07-28 22:35:01 +0000
+++ lib/canonical/launchpad/webapp/adapter.py 2009-07-29 11:37:51 +0000
@@ -134,7 +134,7 @@
134def get_request_statements():134def get_request_statements():
135 """Get the list of executed statements in the request.135 """Get the list of executed statements in the request.
136136
137 The list is composed of (starttime, endtime, statement) tuples.137 The list is composed of (starttime, endtime, db_id, statement) tuples.
138 Times are given in milliseconds since the start of the request.138 Times are given in milliseconds since the start of the request.
139 """139 """
140 return getattr(_local, 'request_statements', [])140 return getattr(_local, 'request_statements', [])
@@ -165,7 +165,11 @@
165 # convert times to integer millisecond values165 # convert times to integer millisecond values
166 starttime = int((starttime - request_starttime) * 1000)166 starttime = int((starttime - request_starttime) * 1000)
167 endtime = int((endtime - request_starttime) * 1000)167 endtime = int((endtime - request_starttime) * 1000)
168 _local.request_statements.append((starttime, endtime, statement))168 # A string containing no whitespace that lets us identify which Store
169 # is being used.
170 database_identifier = connection_wrapper._database.name
171 _local.request_statements.append(
172 (starttime, endtime, database_identifier, statement))
169173
170 # store the last executed statement as an attribute on the current174 # store the last executed statement as an attribute on the current
171 # thread175 # thread
@@ -274,6 +278,8 @@
274 # opinion on what uri is.278 # opinion on what uri is.
275 # pylint: disable-msg=W0231279 # pylint: disable-msg=W0231
276 self._uri = uri280 self._uri = uri
281 # A unique name for this database connection.
282 self.name = uri.database
277283
278 def raw_connect(self):284 def raw_connect(self):
279 # Prevent database connections from the main thread if285 # Prevent database connections from the main thread if
@@ -348,6 +354,9 @@
348354
349class LaunchpadSessionDatabase(Postgres):355class LaunchpadSessionDatabase(Postgres):
350356
357 # A unique name for this database connection.
358 name = 'session'
359
351 def raw_connect(self):360 def raw_connect(self):
352 self._dsn = 'dbname=%s user=%s' % (config.launchpad_session.dbname,361 self._dsn = 'dbname=%s user=%s' % (config.launchpad_session.dbname,
353 config.launchpad_session.dbuser)362 config.launchpad_session.dbuser)
354363
=== modified file 'lib/canonical/launchpad/webapp/errorlog.py'
--- lib/canonical/launchpad/webapp/errorlog.py 2009-07-23 05:02:47 +0000
+++ lib/canonical/launchpad/webapp/errorlog.py 2009-08-05 07:34:55 +0000
@@ -167,9 +167,9 @@
167 fp.write('%s=%s\n' % (urllib.quote(key, safe_chars),167 fp.write('%s=%s\n' % (urllib.quote(key, safe_chars),
168 urllib.quote(value, safe_chars)))168 urllib.quote(value, safe_chars)))
169 fp.write('\n')169 fp.write('\n')
170 for (start, end, statement) in self.db_statements:170 for (start, end, database_id, statement) in self.db_statements:
171 fp.write('%05d-%05d %s\n' % (start, end,171 fp.write('%05d-%05d@%s %s\n' % (
172 _normalise_whitespace(statement)))172 start, end, database_id, _normalise_whitespace(statement)))
173 fp.write('\n')173 fp.write('\n')
174 fp.write(self.tb_text)174 fp.write(self.tb_text)
175175
@@ -206,9 +206,12 @@
206 line = line.strip()206 line = line.strip()
207 if line == '':207 if line == '':
208 break208 break
209 startend, statement = line.split(' ', 1)209 start, end, db_id, statement = re.match(
210 start, end = startend.split('-')210 r'^(\d+)-(\d+)(?:@([\w-]+))?\s+(.*)', line).groups()
211 statements.append((int(start), int(end), statement))211 if db_id is not None:
212 db_id = intern(db_id) # This string is repeated lots.
213 statements.append(
214 (int(start), int(end), db_id, statement))
212215
213 # The rest is traceback.216 # The rest is traceback.
214 tb_text = ''.join(lines)217 tb_text = ''.join(lines)
@@ -471,9 +474,10 @@
471474
472 duration = get_request_duration()475 duration = get_request_duration()
473476
474 statements = sorted((start, end, _safestr(statement))477 statements = sorted(
475 for (start, end, statement)478 (start, end, _safestr(database_id), _safestr(statement))
476 in get_request_statements())479 for (start, end, database_id, statement)
480 in get_request_statements())
477481
478 oopsid, filename = self.newOopsId(now)482 oopsid, filename = self.newOopsId(now)
479483
480484
=== modified file 'lib/canonical/launchpad/webapp/ftests/test_adapter.txt'
--- lib/canonical/launchpad/webapp/ftests/test_adapter.txt 2009-04-17 10:32:16 +0000
+++ lib/canonical/launchpad/webapp/ftests/test_adapter.txt 2009-08-04 12:14:57 +0000
@@ -292,7 +292,7 @@
292 >>> set_request_started()292 >>> set_request_started()
293 >>> store.execute('SELECT 1', noresult=True)293 >>> store.execute('SELECT 1', noresult=True)
294 >>> store.execute('SELECT 2', noresult=True)294 >>> store.execute('SELECT 2', noresult=True)
295 >>> for starttime, endtime, statement in get_request_statements():295 >>> for starttime, endtime, db_id, statement in get_request_statements():
296 ... print statement296 ... print statement
297 SELECT 1297 SELECT 1
298 SELECT 2298 SELECT 2
299299
=== modified file 'lib/canonical/launchpad/webapp/launchpadform.py'
--- lib/canonical/launchpad/webapp/launchpadform.py 2009-06-25 05:30:52 +0000
+++ lib/canonical/launchpad/webapp/launchpadform.py 2009-08-04 00:41:49 +0000
@@ -372,7 +372,7 @@
372372
373 render_context = True373 render_context = True
374374
375 def updateContextFromData(self, data, context=None):375 def updateContextFromData(self, data, context=None, notify_modified=True):
376 """Update the context object based on form data.376 """Update the context object based on form data.
377377
378 If no context is given, the view's context is used.378 If no context is given, the view's context is used.
@@ -386,12 +386,13 @@
386 """386 """
387 if context is None:387 if context is None:
388 context = self.context388 context = self.context
389 context_before_modification = Snapshot(389 if notify_modified:
390 context, providing=providedBy(context))390 context_before_modification = Snapshot(
391 context, providing=providedBy(context))
391392
392 was_changed = form.applyChanges(context, self.form_fields,393 was_changed = form.applyChanges(context, self.form_fields,
393 data, self.adapters)394 data, self.adapters)
394 if was_changed:395 if was_changed and notify_modified:
395 field_names = [form_field.__name__396 field_names = [form_field.__name__
396 for form_field in self.form_fields]397 for form_field in self.form_fields]
397 notify(ObjectModifiedEvent(398 notify(ObjectModifiedEvent(
398399
=== modified file 'lib/canonical/launchpad/webapp/tales.py'
--- lib/canonical/launchpad/webapp/tales.py 2009-08-03 15:21:35 +0000
+++ lib/canonical/launchpad/webapp/tales.py 2009-08-05 02:18:13 +0000
@@ -2757,7 +2757,8 @@
2757 return text2757 return text
2758 result = ['<table class="diff">']2758 result = ['<table class="diff">']
27592759
2760 for row, line in enumerate(text.split('\n')):2760 max_format_lines = config.diff.max_format_lines
2761 for row, line in enumerate(text.splitlines()[:max_format_lines]):
2761 result.append('<tr>')2762 result.append('<tr>')
2762 result.append('<td class="line-no">%s</td>' % (row+1))2763 result.append('<td class="line-no">%s</td>' % (row+1))
2763 if line.startswith('==='):2764 if line.startswith('==='):
27642765
=== modified file 'lib/canonical/launchpad/webapp/tests/test_errorlog.py'
--- lib/canonical/launchpad/webapp/tests/test_errorlog.py 2009-07-17 00:26:05 +0000
+++ lib/canonical/launchpad/webapp/tests/test_errorlog.py 2009-07-29 14:28:18 +0000
@@ -57,8 +57,8 @@
57 'pageid', 'traceback-text', 'username', 'url', 42,57 'pageid', 'traceback-text', 'username', 'url', 42,
58 [('name1', 'value1'), ('name2', 'value2'),58 [('name1', 'value1'), ('name2', 'value2'),
59 ('name1', 'value3')],59 ('name1', 'value3')],
60 [(1, 5, 'SELECT 1'),60 [(1, 5, 'store_a', 'SELECT 1'),
61 (5, 10, 'SELECT 2')])61 (5, 10, 'store_b', 'SELECT 2')])
62 self.assertEqual(entry.id, 'id')62 self.assertEqual(entry.id, 'id')
63 self.assertEqual(entry.type, 'exc-type')63 self.assertEqual(entry.type, 'exc-type')
64 self.assertEqual(entry.value, 'exc-value')64 self.assertEqual(entry.value, 'exc-value')
@@ -74,8 +74,12 @@
74 self.assertEqual(entry.req_vars[1], ('name2', 'value2'))74 self.assertEqual(entry.req_vars[1], ('name2', 'value2'))
75 self.assertEqual(entry.req_vars[2], ('name1', 'value3'))75 self.assertEqual(entry.req_vars[2], ('name1', 'value3'))
76 self.assertEqual(len(entry.db_statements), 2)76 self.assertEqual(len(entry.db_statements), 2)
77 self.assertEqual(entry.db_statements[0], (1, 5, 'SELECT 1'))77 self.assertEqual(
78 self.assertEqual(entry.db_statements[1], (5, 10, 'SELECT 2'))78 entry.db_statements[0],
79 (1, 5, 'store_a', 'SELECT 1'))
80 self.assertEqual(
81 entry.db_statements[1],
82 (5, 10, 'store_b', 'SELECT 2'))
7983
80 def test_write(self):84 def test_write(self):
81 """Test ErrorReport.write()"""85 """Test ErrorReport.write()"""
@@ -88,8 +92,8 @@
88 [('HTTP_USER_AGENT', 'Mozilla/5.0'),92 [('HTTP_USER_AGENT', 'Mozilla/5.0'),
89 ('HTTP_REFERER', 'http://localhost:9000/'),93 ('HTTP_REFERER', 'http://localhost:9000/'),
90 ('name=foo', 'hello\nworld')],94 ('name=foo', 'hello\nworld')],
91 [(1, 5, 'SELECT 1'),95 [(1, 5, 'store_a', 'SELECT 1'),
92 (5, 10, 'SELECT\n2')])96 (5, 10,'store_b', 'SELECT\n2')])
93 fp = StringIO.StringIO()97 fp = StringIO.StringIO()
94 entry.write(fp)98 entry.write(fp)
95 self.assertEqual(fp.getvalue(), dedent("""\99 self.assertEqual(fp.getvalue(), dedent("""\
@@ -108,13 +112,58 @@
108 HTTP_REFERER=http://localhost:9000/112 HTTP_REFERER=http://localhost:9000/
109 name%%3Dfoo=hello%%0Aworld113 name%%3Dfoo=hello%%0Aworld
110114
111 00001-00005 SELECT 1115 00001-00005@store_a SELECT 1
112 00005-00010 SELECT 2116 00005-00010@store_b SELECT 2
113117
114 traceback-text""" % (versioninfo.branch_nick, versioninfo.revno)))118 traceback-text""" % (versioninfo.branch_nick, versioninfo.revno)))
115119
116 def test_read(self):120 def test_read(self):
117 """Test ErrorReport.read()"""121 """Test ErrorReport.read()."""
122 fp = StringIO.StringIO(dedent("""\
123 Oops-Id: OOPS-A0001
124 Exception-Type: NotFound
125 Exception-Value: error message
126 Date: 2005-04-01T00:00:00+00:00
127 Page-Id: IFoo:+foo-template
128 User: Sample User
129 URL: http://localhost:9000/foo
130 Duration: 42
131
132 HTTP_USER_AGENT=Mozilla/5.0
133 HTTP_REFERER=http://localhost:9000/
134 name%3Dfoo=hello%0Aworld
135
136 00001-00005@store_a SELECT 1
137 00005-00010@store_b SELECT 2
138
139 traceback-text"""))
140 entry = ErrorReport.read(fp)
141 self.assertEqual(entry.id, 'OOPS-A0001')
142 self.assertEqual(entry.type, 'NotFound')
143 self.assertEqual(entry.value, 'error message')
144 self.assertEqual(entry.time, datetime.datetime(2005, 4, 1))
145 self.assertEqual(entry.pageid, 'IFoo:+foo-template')
146 self.assertEqual(entry.tb_text, 'traceback-text')
147 self.assertEqual(entry.username, 'Sample User')
148 self.assertEqual(entry.url, 'http://localhost:9000/foo')
149 self.assertEqual(entry.duration, 42)
150 self.assertEqual(len(entry.req_vars), 3)
151 self.assertEqual(entry.req_vars[0], ('HTTP_USER_AGENT',
152 'Mozilla/5.0'))
153 self.assertEqual(entry.req_vars[1], ('HTTP_REFERER',
154 'http://localhost:9000/'))
155 self.assertEqual(entry.req_vars[2], ('name=foo', 'hello\nworld'))
156 self.assertEqual(len(entry.db_statements), 2)
157 self.assertEqual(
158 entry.db_statements[0],
159 (1, 5, 'store_a', 'SELECT 1'))
160 self.assertEqual(
161 entry.db_statements[1],
162 (5, 10, 'store_b', 'SELECT 2'))
163
164
165 def test_read_no_store_id(self):
166 """Test ErrorReport.read() for old logs with no store_id."""
118 fp = StringIO.StringIO(dedent("""\167 fp = StringIO.StringIO(dedent("""\
119 Oops-Id: OOPS-A0001168 Oops-Id: OOPS-A0001
120 Exception-Type: NotFound169 Exception-Type: NotFound
@@ -137,8 +186,6 @@
137 self.assertEqual(entry.id, 'OOPS-A0001')186 self.assertEqual(entry.id, 'OOPS-A0001')
138 self.assertEqual(entry.type, 'NotFound')187 self.assertEqual(entry.type, 'NotFound')
139 self.assertEqual(entry.value, 'error message')188 self.assertEqual(entry.value, 'error message')
140 # XXX jamesh 2005-11-30:
141 # this should probably convert back to a datetime
142 self.assertEqual(entry.time, datetime.datetime(2005, 4, 1))189 self.assertEqual(entry.time, datetime.datetime(2005, 4, 1))
143 self.assertEqual(entry.pageid, 'IFoo:+foo-template')190 self.assertEqual(entry.pageid, 'IFoo:+foo-template')
144 self.assertEqual(entry.tb_text, 'traceback-text')191 self.assertEqual(entry.tb_text, 'traceback-text')
@@ -152,8 +199,8 @@
152 'http://localhost:9000/'))199 'http://localhost:9000/'))
153 self.assertEqual(entry.req_vars[2], ('name=foo', 'hello\nworld'))200 self.assertEqual(entry.req_vars[2], ('name=foo', 'hello\nworld'))
154 self.assertEqual(len(entry.db_statements), 2)201 self.assertEqual(len(entry.db_statements), 2)
155 self.assertEqual(entry.db_statements[0], (1, 5, 'SELECT 1'))202 self.assertEqual(entry.db_statements[0], (1, 5, None, 'SELECT 1'))
156 self.assertEqual(entry.db_statements[1], (5, 10, 'SELECT 2'))203 self.assertEqual(entry.db_statements[1], (5, 10, None, 'SELECT 2'))
157204
158205
159class TestErrorReportingUtility(unittest.TestCase):206class TestErrorReportingUtility(unittest.TestCase):
@@ -422,7 +469,7 @@
422 # Test ErrorReportingUtility.raising() with an XML-RPC request.469 # Test ErrorReportingUtility.raising() with an XML-RPC request.
423 request = TestRequest()470 request = TestRequest()
424 directlyProvides(request, IXMLRPCRequest)471 directlyProvides(request, IXMLRPCRequest)
425 request.getPositionalArguments = lambda : (1,2)472 request.getPositionalArguments = lambda: (1, 2)
426 utility = ErrorReportingUtility()473 utility = ErrorReportingUtility()
427 now = datetime.datetime(2006, 04, 01, 00, 30, 00, tzinfo=UTC)474 now = datetime.datetime(2006, 04, 01, 00, 30, 00, tzinfo=UTC)
428 try:475 try:
@@ -469,7 +516,6 @@
469 utility.raising(sys.exc_info(), request, now=now)516 utility.raising(sys.exc_info(), request, now=now)
470 self.assertEqual(request.oopsid, None)517 self.assertEqual(request.oopsid, None)
471518
472
473 def test_raising_for_script(self):519 def test_raising_for_script(self):
474 """Test ErrorReportingUtility.raising with a ScriptRequest."""520 """Test ErrorReportingUtility.raising with a ScriptRequest."""
475 utility = ErrorReportingUtility()521 utility = ErrorReportingUtility()
@@ -753,7 +799,7 @@
753 # logged will have OOPS reports generated for them.799 # logged will have OOPS reports generated for them.
754 error_message = self.factory.getUniqueString()800 error_message = self.factory.getUniqueString()
755 try:801 try:
756 1/0802 ignored = 1/0
757 except ZeroDivisionError:803 except ZeroDivisionError:
758 self.logger.exception(error_message)804 self.logger.exception(error_message)
759 oops_report = self.error_utility.getLastOopsReport()805 oops_report = self.error_utility.getLastOopsReport()
760806
=== modified file 'lib/canonical/launchpad/webapp/tests/test_tales.py'
--- lib/canonical/launchpad/webapp/tests/test_tales.py 2009-06-25 05:30:52 +0000
+++ lib/canonical/launchpad/webapp/tests/test_tales.py 2009-08-05 01:06:49 +0000
@@ -10,12 +10,13 @@
10from zope.security.proxy import removeSecurityProxy10from zope.security.proxy import removeSecurityProxy
11from zope.testing.doctestunit import DocTestSuite11from zope.testing.doctestunit import DocTestSuite
1212
13from canonical.config import config
13from canonical.launchpad.ftests import test_tales14from canonical.launchpad.ftests import test_tales
14from lp.testing import login, TestCase, TestCaseWithFactory
15from canonical.launchpad.testing.pages import find_tags_by_class15from canonical.launchpad.testing.pages import find_tags_by_class
16from canonical.launchpad.webapp.tales import FormattersAPI16from canonical.launchpad.webapp.tales import FormattersAPI
17from canonical.testing import (17from canonical.testing import (
18 DatabaseFunctionalLayer, LaunchpadFunctionalLayer)18 DatabaseFunctionalLayer, LaunchpadFunctionalLayer)
19from lp.testing import login, TestCase, TestCaseWithFactory
1920
2021
21def test_requestapi():22def test_requestapi():
@@ -209,6 +210,13 @@
209 '<td class="text"> </td></tr></table>',210 '<td class="text"> </td></tr></table>',
210 FormattersAPI(' ').format_diff())211 FormattersAPI(' ').format_diff())
211212
213 def test_format_unicode(self):
214 # Sometimes the strings contain unicode, those should work too.
215 self.assertEqual(
216 u'<table class="diff"><tr><td class="line-no">1</td>'
217 u'<td class="text">Unicode \u1010</td></tr></table>',
218 FormattersAPI(u'Unicode \u1010').format_diff())
219
212 def test_cssClasses(self):220 def test_cssClasses(self):
213 # Different parts of the diff have different css classes.221 # Different parts of the diff have different css classes.
214 diff = dedent('''\222 diff = dedent('''\
@@ -240,6 +248,25 @@
240 'diff-comment text'],248 'diff-comment text'],
241 [str(tag['class']) for tag in text])249 [str(tag['class']) for tag in text])
242250
251 def test_config_value_limits_line_count(self):
252 # The config.diff.max_line_format contains the maximum number of lines
253 # to format.
254 diff = dedent('''\
255 === modified file 'tales.py'
256 --- tales.py
257 +++ tales.py
258 @@ -2435,6 +2435,8 @@
259 def format_diff(self):
260 - removed this line
261 + added this line
262 ########
263 # A merge directive comment.
264 ''')
265 self.pushConfig("diff", max_format_lines=3)
266 html = FormattersAPI(diff).format_diff()
267 line_count = html.count('<td class="line-no">')
268 self.assertEqual(3, line_count)
269
243270
244class TestPreviewDiffFormatter(TestCaseWithFactory):271class TestPreviewDiffFormatter(TestCaseWithFactory):
245 """Test the PreviewDiffFormatterAPI class."""272 """Test the PreviewDiffFormatterAPI class."""
246273
=== modified file 'lib/lp/bugs/browser/bugtask.py'
--- lib/lp/bugs/browser/bugtask.py 2009-07-24 10:11:20 +0000
+++ lib/lp/bugs/browser/bugtask.py 2009-08-04 14:57:07 +0000
@@ -2915,13 +2915,15 @@
2915 return self.context.isUserAffected(self.user)2915 return self.context.isUserAffected(self.user)
29162916
2917 @property2917 @property
2918 def affects_form_value(self):2918 def current_user_affected_js_status(self):
2919 """The value to use in the inline me too form."""2919 """A javascript literal indicating if the user is affected."""
2920 affected = self.context.isUserAffected(self.user)2920 affected = self.current_user_affected_status
2921 if affected is None or affected == False:2921 if affected is None:
2922 return 'YES'2922 return 'null'
2923 elif affected:
2924 return 'true'
2923 else:2925 else:
2924 return 'NO'2926 return 'false'
29252927
29262928
2927class BugTaskTableRowView(LaunchpadView):2929class BugTaskTableRowView(LaunchpadView):
29282930
=== modified file 'lib/lp/bugs/browser/tests/test_bugtask.py'
--- lib/lp/bugs/browser/tests/test_bugtask.py 2009-06-25 00:40:31 +0000
+++ lib/lp/bugs/browser/tests/test_bugtask.py 2009-07-23 10:34:08 +0000
@@ -1,23 +1,63 @@
1# Copyright 2009 Canonical Ltd. This software is licensed under the1# Copyright 2009 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4__metaclass__ = type
5
6
4import unittest7import unittest
58
6from zope.testing.doctest import DocTestSuite9from zope.testing.doctest import DocTestSuite
710
8from lp.bugs.browser import bugtask11from canonical.launchpad.ftests import login
9from canonical.launchpad.testing.systemdocs import (12from canonical.launchpad.testing.systemdocs import (
10 LayeredDocFileSuite, setUp, tearDown)13 LayeredDocFileSuite, setUp, tearDown)
11from canonical.testing import LaunchpadFunctionalLayer14from canonical.testing import LaunchpadFunctionalLayer
1215
16from lp.bugs.browser import bugtask
17from lp.bugs.browser.bugtask import BugTasksAndNominationsView
18from lp.testing import TestCaseWithFactory
19
20
21class TestBugTasksAndNominationsView(TestCaseWithFactory):
22
23 layer = LaunchpadFunctionalLayer
24
25 def setUp(self):
26 super(TestBugTasksAndNominationsView, self).setUp()
27 login('foo.bar@canonical.com')
28 self.bug = self.factory.makeBug()
29 self.view = BugTasksAndNominationsView(self.bug, None)
30
31 def test_current_user_affected_status(self):
32 self.failUnlessEqual(
33 None, self.view.current_user_affected_status)
34 self.view.context.markUserAffected(self.view.user, True)
35 self.failUnlessEqual(
36 True, self.view.current_user_affected_status)
37 self.view.context.markUserAffected(self.view.user, False)
38 self.failUnlessEqual(
39 False, self.view.current_user_affected_status)
40
41 def test_current_user_affected_js_status(self):
42 self.failUnlessEqual(
43 'null', self.view.current_user_affected_js_status)
44 self.view.context.markUserAffected(self.view.user, True)
45 self.failUnlessEqual(
46 'true', self.view.current_user_affected_js_status)
47 self.view.context.markUserAffected(self.view.user, False)
48 self.failUnlessEqual(
49 'false', self.view.current_user_affected_js_status)
50
1351
14def test_suite():52def test_suite():
15 suite = unittest.TestSuite()53 suite = unittest.TestSuite()
54 suite.addTest(unittest.makeSuite(TestBugTasksAndNominationsView))
16 suite.addTest(DocTestSuite(bugtask))55 suite.addTest(DocTestSuite(bugtask))
17 suite.addTest(LayeredDocFileSuite(56 suite.addTest(LayeredDocFileSuite(
18 'bugtask-target-link-titles.txt', setUp=setUp, tearDown=tearDown,57 'bugtask-target-link-titles.txt', setUp=setUp, tearDown=tearDown,
19 layer=LaunchpadFunctionalLayer))58 layer=LaunchpadFunctionalLayer))
20 return suite59 return suite
2160
61
22if __name__ == '__main__':62if __name__ == '__main__':
23 unittest.TextTestRunner().run(test_suite())63 unittest.TextTestRunner().run(test_suite())
2464
=== modified file 'lib/lp/bugs/doc/bugnotification-email.txt'
--- lib/lp/bugs/doc/bugnotification-email.txt 2009-06-12 16:36:02 +0000
+++ lib/lp/bugs/doc/bugnotification-email.txt 2009-07-22 09:12:01 +0000
@@ -6,7 +6,9 @@
6themselves; for that, see bugnotifications.txt.6themselves; for that, see bugnotifications.txt.
77
8The reference spec associated with this document is available on the8The reference spec associated with this document is available on the
9Launchpad wiki. <https://launchpad.canonical.com/FormattingBugNotifications>9Launchpad development wiki:
10
11 https://dev.launchpad.net/Bugs/Specs/FormattingBugNotifications
1012
11You need to be logged in to edit bugs in Malone, so let's get started:13You need to be logged in to edit bugs in Malone, so let's get started:
1214
1315
=== modified file 'lib/lp/bugs/externalbugtracker/mantis.py'
--- lib/lp/bugs/externalbugtracker/mantis.py 2009-06-25 00:40:31 +0000
+++ lib/lp/bugs/externalbugtracker/mantis.py 2009-07-22 09:12:01 +0000
@@ -80,7 +80,9 @@
80 """An `ExternalBugTracker` for dealing with Mantis instances.80 """An `ExternalBugTracker` for dealing with Mantis instances.
8181
82 For a list of tested Mantis instances and their behaviour when82 For a list of tested Mantis instances and their behaviour when
83 exported from, see http://launchpad.canonical.com/MantisBugtrackers.83 exported from, see:
84
85 https://dev.launchpad.net/Bugs/ExternalBugTrackers/Mantis
84 """86 """
8587
86 # Custom opener that automatically sends anonymous credentials to88 # Custom opener that automatically sends anonymous credentials to
8789
=== modified file 'lib/lp/bugs/stories/bugs/xx-bug-affects-me-too.txt'
--- lib/lp/bugs/stories/bugs/xx-bug-affects-me-too.txt 2009-06-12 16:36:02 +0000
+++ lib/lp/bugs/stories/bugs/xx-bug-affects-me-too.txt 2009-07-31 13:49:53 +0000
@@ -1,7 +1,7 @@
1= Marking a bug as affecting the user =1= Marking a bug as affecting the user =
22
3Users can mark bugs as affecting them. Let's create a sample bug to try3Users can mark bugs as affecting them. Let's create a sample bug to
4this on.4try this out.
55
6 >>> login(ANONYMOUS)6 >>> login(ANONYMOUS)
7 >>> from canonical.launchpad.webapp import canonical_url7 >>> from canonical.launchpad.webapp import canonical_url
@@ -9,15 +9,27 @@
9 >>> test_bug_url = canonical_url(test_bug)9 >>> test_bug_url = canonical_url(test_bug)
10 >>> logout()10 >>> logout()
1111
12The user goes to the bug's index page, and clicks the edit action link12The user goes to the bug's index page, and finds a statement that the
13near 'This bug affects me too'.13bug is not marked as affecting them.
1414
15 >>> user_browser.open(test_bug_url)15 >>> user_browser.open(test_bug_url)
16 >>> print extract_text(find_tag_by_id(16 >>> print extract_text(find_tag_by_id(
17 ... user_browser.contents, 'affectsmetooform'))17 ... user_browser.contents, 'affectsmetoo').find(
18 This bug doesn't affect me...18 ... None, 'static'))
1919 This bug doesn't affect me
20 >>> user_browser.getLink('change').click()20
21Next to the statement is a link containing an edit icon.
22
23 >>> edit_link = find_tag_by_id(
24 ... user_browser.contents, 'affectsmetoo').a
25 >>> print edit_link['href']
26 +affectsmetoo
27 >>> print edit_link.img['src']
28 /@@/edit
29
30The user is affected by this bug, so clicks the link.
31
32 >>> user_browser.getLink(url='+affectsmetoo').click()
21 >>> print user_browser.url33 >>> print user_browser.url
22 http://bugs.launchpad.dev/.../+bug/.../+affectsmetoo34 http://bugs.launchpad.dev/.../+bug/.../+affectsmetoo
23 >>> user_browser.getControl(name='field.affects').value35 >>> user_browser.getControl(name='field.affects').value
@@ -28,22 +40,26 @@
28 >>> user_browser.getControl('Change').click()40 >>> user_browser.getControl('Change').click()
2941
30The bug page loads again, and now the text is changed, to make it42The bug page loads again, and now the text is changed, to make it
31clear to the user that they can change the selection.43clear to the user that they have marked this bug as affecting them.
3244
33 >>> print extract_text(find_tags_by_class(45 >>> print extract_text(find_tag_by_id(
34 ... user_browser.contents, 'menu-link-affectsmetoo')[0])46 ... user_browser.contents, 'affectsmetoo').find(
35 change47 ... None, 'static'))
48 This bug affects me too
3649
37Next to it, we also see the 'hot bug' icon, to indicate that the user50Next to it, we also see the 'hot bug' icon, to indicate that the user
38has marked the bug as affecting them.51has marked the bug as affecting them.
3952
40 >>> print find_tag_by_id(53 >>> print find_tag_by_id(
41 ... user_browser.contents, 'affectsmetooform').img['src']54 ... user_browser.contents, 'affectsmetoo').img['src']
42 /@@/flame-icon55 /@@/flame-icon
4356
44 >>> user_browser.getLink('change').click()57On second thoughts, the user realises that this bug does not affect
4558them, so they click on the edit link once more.
46The user is changing his selection to 'No' and submits the form.59
60 >>> user_browser.getLink(url='+affectsmetoo').click()
61
62The user changes his selection to 'No' and submits the form.
4763
48 >>> user_browser.getControl(name='field.affects').value = ['NO']64 >>> user_browser.getControl(name='field.affects').value = ['NO']
49 >>> user_browser.getControl('Change').click()65 >>> user_browser.getControl('Change').click()
@@ -51,18 +67,43 @@
51Back at the bug page, the text changes once again.67Back at the bug page, the text changes once again.
5268
53 >>> print extract_text(find_tag_by_id(69 >>> print extract_text(find_tag_by_id(
54 ... user_browser.contents, 'affectsmetooform'))70 ... user_browser.contents, 'affectsmetoo').find(
55 This bug doesn't affect me...71 ... None, 'static'))
5672 This bug doesn't affect me
57== One-click interaction ==73
5874
59If the user's browser provides javascript, they don't need to go to75== Static and dynamic support ==
60another page to change their selection. Instead, an in-page form is76
61submitted when they click the action link.77A bug page contains markup to support both static (no Javascript) and
6278dynamic (Javascript enabled) scenarios.
63 >>> me_too_form = user_browser.getForm(id='affectsmetooform')79
64 >>> user_browser.getControl(name='field.affects').value80 >>> def class_filter(css_class):
65 'YES'81 ... def test(node):
66 >>> me_too_form.submit()82 ... return css_class in node.get('class', '').split()
67 >>> user_browser.getControl(name='field.affects').value83 ... return test
68 'NO'84
85 >>> static_content = find_tag_by_id(
86 ... user_browser.contents, 'affectsmetoo').find(
87 ... class_filter('static'))
88
89 >>> static_content is not None
90 True
91
92 >>> dynamic_content = find_tag_by_id(
93 ... user_browser.contents, 'affectsmetoo').find(
94 ... class_filter('dynamic'))
95
96 >>> dynamic_content is not None
97 True
98
99The dynamic content is hidden by the presence of the "unseen" CSS
100class.
101
102 >>> print static_content.get('class')
103 static
104
105 >>> print dynamic_content.get('class')
106 dynamic unseen
107
108It is the responsibilty of Javascript running in the page to unhide
109the dynamic content and hide the static content.
69110
=== modified file 'lib/lp/bugs/templates/bugtasks-and-nominations-table.pt'
--- lib/lp/bugs/templates/bugtasks-and-nominations-table.pt 2009-07-17 17:59:07 +0000
+++ lib/lp/bugs/templates/bugtasks-and-nominations-table.pt 2009-08-04 14:51:23 +0000
@@ -38,11 +38,9 @@
3838
39</table>39</table>
4040
41<div41<div class="actions"
42 class="actions"42 tal:define="current_bugtask view/current_bugtask"
43 tal:define="current_bugtask view/current_bugtask"43 tal:condition="view/displayAlsoAffectsLinks">
44
45 tal:condition="view/displayAlsoAffectsLinks">
46 <tal:also-affects-links define="context_menu context/menu:context">44 <tal:also-affects-links define="context_menu context/menu:context">
47 <tal:addupstream45 <tal:addupstream
48 define="link context_menu/addupstream"46 define="link context_menu/addupstream"
@@ -56,56 +54,50 @@
56 define="link context_menu/nominate"54 define="link context_menu/nominate"
57 condition="link/enabled"55 condition="link/enabled"
58 replace="structure link/render" />56 replace="structure link/render" />
59 <form57 <span id="affectsmetoo" style="display: inline"
60 id="affectsmetooform"58 tal:condition="link/enabled"
61 name="affectsmetooform"59 tal:define="link context_menu/affectsmetoo;
62 method="post"60 affected view/current_user_affected_status">
63 enctype="multipart/form-data"61
64 accept-charset="UTF-8"62 <tal:comment condition="nothing">
65 tal:define="link context_menu/affectsmetoo"63 This .static section is shown in browsers with javascript
66 tal:condition="link/enabled"64 enabled, and before setup_me_too is run.
67 tal:attributes="action link/url"65 </tal:comment>
68 style="display: inline">66 <span class="static">
69 <input67 <tal:affected condition="affected">
70 name="field.affects"68 <img width="14" height="14" src="/@@/flame-icon" alt="" />
71 type="hidden"69 This bug affects me too
72 tal:attributes="value view/affects_form_value" />70 </tal:affected>
73 <input71 <tal:not-affected condition="not:affected">
74 type="hidden"72 This bug doesn't affect me
75 name="field.actions.change"73 </tal:not-affected>
76 value="" />74 <a href="+affectsmetoo">
77 <tal:affected condition="view/current_user_affected_status">75 <img class="editicon" src="/@@/edit" alt="Edit" />
78 <img width="14" height="14" src="/@@/flame-icon" alt="" />76 </a>
79 This bug affects me too77 </span>
80 </tal:affected>78
81 <tal:affected condition="not: view/current_user_affected_status">79 <tal:comment condition="nothing">
82 This bug doesn't affect me80 This .dynamic section is used by setup_me_too to display
83 </tal:affected>81 controls and information in the correct places.
84 (<tal:affectsmetoo82 </tal:comment>
85 define="link context_menu/affectsmetoo"83 <span class="dynamic unseen">
86 condition="link/enabled"84 <img src="/@@/flame-icon" alt=""/>
87 replace="structure link/render" />)85 <a href="+affectsmetoo" class="js-action"
88 <tal:nothing condition="nothing">86 ><span class="value">Does this bug affect you?</span></a>
89 The following is a trick to allow users to mark87 <img class="editicon" src="/@@/edit" alt="Edit" />
90 themselves as affected by a bug with only one click.88 </span>
91 If Javascript is available, we submit the form and89
92 immediately go back to the same page, saving them the90 <script type="text/javascript" tal:content="string:
93 need to go to a new page.91 YUI().use('event', 'bugs.bugtask_index', function(Y) {
94 </tal:nothing>92 Y.on('load', function(e) {
95 <script type="text/javascript">93 Y.bugs.setup_me_too(${view/current_user_affected_js_status});
96 function sendMeTooForm(e) {94 }, window);
97 $('affectsmetooform').submit();95 });
98 e.preventDefault();96 ">
99 }
100 function connectMeTooLink() {
101 var me_too_link = getFirstElementByTagAndClassName(
102 'a', 'menu-link-affectsmetoo');
103 connect(me_too_link, 'onclick', sendMeTooForm);
104 }
105 registerLaunchpadFunction(connectMeTooLink);
106 </script>97 </script>
107 </form>98
99 </span>
108 </tal:also-affects-links>100 </tal:also-affects-links>
109
110</div>101</div>
102
111</tal:root>103</tal:root>
112104
=== added file 'lib/lp/bugs/windmill/tests/test_bugs/test_bug_me_too.py'
--- lib/lp/bugs/windmill/tests/test_bugs/test_bug_me_too.py 1970-01-01 00:00:00 +0000
+++ lib/lp/bugs/windmill/tests/test_bugs/test_bug_me_too.py 2009-07-31 15:27:05 +0000
@@ -0,0 +1,65 @@
1# Copyright 2009 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4from canonical.launchpad.windmill.testing import lpuser
5
6from windmill.authoring import WindmillTestClient
7
8
9def test_me_too():
10 """Test the "this bug affects me too" options on bug pages.
11
12 This test ensures that, with Javascript enabled, the "me too"
13 status can be edited in-page.
14 """
15 client = WindmillTestClient('Bug "me too" test')
16 lpuser.SAMPLE_PERSON.ensure_login(client)
17
18 # Open bug 11 and wait for it to finish loading.
19 client.open(url=u'http://bugs.launchpad.dev:8085/jokosher/+bug/11/+index')
20 client.waits.forPageLoad(timeout=u'20000')
21
22 # Wait for setup_me_too to sort out the "me too" elements.
23 client.waits.forElement(
24 xpath=(u"//span[@id='affectsmetoo' and "
25 u"@class='yui-metoocs-content']"))
26
27 # Currently this bug does not affect the logged-in user.
28 client.asserts.assertText(
29 xpath=u"//span[@id='affectsmetoo']/span[@class='value']",
30 validator=u"This bug doesn't affect me")
31
32 # There is an edit icon next to the text which can be clicked to
33 # edit the "me too" status. However, we won't click it with
34 # Windmill because the widget actually responds to mouse-down, and
35 # Windmill seems to do something funny instead.
36 client.mouseDown(
37 xpath=u"//span[@id='affectsmetoo']//img[@class='editicon']")
38 client.mouseUp(
39 xpath=u"//span[@id='affectsmetoo']//img[@class='editicon']")
40
41 # Wait for the modal dialog to appear.
42 client.waits.forElement(id=u'yui-pretty-overlay-modal')
43
44 # There's a close button if we change our mind.
45 client.click(
46 xpath=(u"//div[@id='yui-pretty-overlay-modal']//"
47 u"a[@class='close-button']"))
48
49 # Wait for the modal dialog to disappear. Unfortunately the test
50 # below doesn't work, nor does testing clientWidth, or anything I
51 # could think of, so it's commented out for now because chasing
52 # this is not a good use of time.
53
54 # client.asserts.assertElemJS(
55 # id=u'yui-pretty-overlay-modal',
56 # js=(u'getComputedStyle(element, '
57 # u'"visibility").visibility == "hidden"'))
58
59 # However, we want to mark this bug as affecting the logged-in
60 # user. We can also click on the content box of the "me too"
61 # widget; we are not forced to use the edit icon.
62 client.click(xpath=u"//span[@id='affectsmetoo']")
63 client.waits.forElement(id=u'yui-pretty-overlay-modal')
64
65 # XXX: Gavin Panella bug=361097 2009-07-31: Finish this.
066
=== modified file 'lib/lp/code/browser/branch.py'
--- lib/lp/code/browser/branch.py 2009-07-19 04:41:14 +0000
+++ lib/lp/code/browser/branch.py 2009-08-04 00:41:49 +0000
@@ -36,12 +36,15 @@
36from zope.app.form.browser import TextAreaWidget36from zope.app.form.browser import TextAreaWidget
37from zope.traversing.interfaces import IPathAdapter37from zope.traversing.interfaces import IPathAdapter
38from zope.component import getUtility, queryAdapter38from zope.component import getUtility, queryAdapter
39from zope.event import notify
39from zope.formlib import form40from zope.formlib import form
40from zope.interface import Interface, implements41from zope.interface import Interface, implements, providedBy
41from zope.publisher.interfaces import NotFound42from zope.publisher.interfaces import NotFound
42from zope.schema import Choice, Text43from zope.schema import Choice, Text
43from lazr.delegates import delegates44from lazr.delegates import delegates
44from lazr.enum import EnumeratedType, Item45from lazr.enum import EnumeratedType, Item
46from lazr.lifecycle.event import ObjectModifiedEvent
47from lazr.lifecycle.snapshot import Snapshot
45from lazr.uri import URI48from lazr.uri import URI
4649
47from canonical.cachedproperty import cachedproperty50from canonical.cachedproperty import cachedproperty
@@ -574,7 +577,6 @@
574 the user to be able to edit it.577 the user to be able to edit it.
575 """578 """
576 use_template(IBranch, include=[579 use_template(IBranch, include=[
577 'owner',
578 'name',580 'name',
579 'url',581 'url',
580 'description',582 'description',
@@ -582,6 +584,7 @@
582 'whiteboard',584 'whiteboard',
583 ])585 ])
584 private = copy_field(IBranch['private'], readonly=False)586 private = copy_field(IBranch['private'], readonly=False)
587 owner = copy_field(IBranch['owner'], readonly=False)
585588
586589
587class BranchEditFormView(LaunchpadEditFormView):590class BranchEditFormView(LaunchpadEditFormView):
@@ -598,9 +601,16 @@
598 @action('Change Branch', name='change')601 @action('Change Branch', name='change')
599 def change_action(self, action, data):602 def change_action(self, action, data):
600 # If the owner or product has changed, add an explicit notification.603 # If the owner or product has changed, add an explicit notification.
604 # We take our own snapshot here to make sure that the snapshot records
605 # changes to the owner and private, and we notify the listeners
606 # explicitly below rather than the notification that would normally be
607 # sent in updateContextFromData.
608 branch_before_modification = Snapshot(
609 self.context, providing=providedBy(self.context))
601 if 'owner' in data:610 if 'owner' in data:
602 new_owner = data['owner']611 new_owner = data.pop('owner')
603 if new_owner != self.context.owner:612 if new_owner != self.context.owner:
613 self.context.setOwner(new_owner, self.user)
604 self.request.response.addNotification(614 self.request.response.addNotification(
605 "The branch owner has been changed to %s (%s)"615 "The branch owner has been changed to %s (%s)"
606 % (new_owner.displayname, new_owner.name))616 % (new_owner.displayname, new_owner.name))
@@ -616,7 +626,13 @@
616 else:626 else:
617 self.request.response.addNotification(627 self.request.response.addNotification(
618 "The branch is now publicly accessible.")628 "The branch is now publicly accessible.")
619 if self.updateContextFromData(data):629 if self.updateContextFromData(data, notify_modified=False):
630 # Notify the object has changed with the snapshot that was taken
631 # earler.
632 field_names = [
633 form_field.__name__ for form_field in self.form_fields]
634 notify(ObjectModifiedEvent(
635 self.context, branch_before_modification, field_names))
620 # Only specify that the context was modified if there636 # Only specify that the context was modified if there
621 # was in fact a change.637 # was in fact a change.
622 self.context.date_last_modified = UTC_NOW638 self.context.date_last_modified = UTC_NOW
623639
=== modified file 'lib/lp/code/browser/branchmergeproposal.py'
--- lib/lp/code/browser/branchmergeproposal.py 2009-07-24 03:52:27 +0000
+++ lib/lp/code/browser/branchmergeproposal.py 2009-08-05 02:18:13 +0000
@@ -42,6 +42,7 @@
42from lazr.lifecycle.event import ObjectModifiedEvent42from lazr.lifecycle.event import ObjectModifiedEvent
4343
44from canonical.cachedproperty import cachedproperty44from canonical.cachedproperty import cachedproperty
45from canonical.config import config
4546
46from canonical.launchpad import _47from canonical.launchpad import _
47from lp.code.adapters.branch import BranchMergeProposalDelta48from lp.code.adapters.branch import BranchMergeProposalDelta
@@ -372,7 +373,7 @@
372 result.append(dict(style=style, comment=comment))373 result.append(dict(style=style, comment=comment))
373 return result374 return result
374375
375 @property376 @cachedproperty
376 def review_diff(self):377 def review_diff(self):
377 """Return a (hopefully) intelligently encoded review diff."""378 """Return a (hopefully) intelligently encoded review diff."""
378 if self.context.review_diff is None:379 if self.context.review_diff is None:
@@ -382,7 +383,25 @@
382 except UnicodeDecodeError:383 except UnicodeDecodeError:
383 diff = self.context.review_diff.diff.text.decode('windows-1252',384 diff = self.context.review_diff.diff.text.decode('windows-1252',
384 'replace')385 'replace')
385 return diff386 # Strip off the trailing carriage returns.
387 return diff.rstrip('\n')
388
389 @cachedproperty
390 def review_diff_oversized(self):
391 """Return True if the review_diff is over the configured size limit.
392
393 The diff can be over the limit in two ways. If the diff is oversized
394 in bytes it will be cut off at the Diff.text method. If the number of
395 lines is over the max_format_lines, then it is cut off at the fmt:diff
396 processing.
397 """
398 review_diff = self.context.review_diff
399 if review_diff is None:
400 return False
401 if review_diff.diff.oversized:
402 return True
403 diff_text = self.review_diff
404 return diff_text.count('\n') >= config.diff.max_format_lines
386405
387 @property406 @property
388 def has_bug_or_spec(self):407 def has_bug_or_spec(self):
389408
=== modified file 'lib/lp/code/browser/tests/test_branchmergeproposallisting.py'
--- lib/lp/code/browser/tests/test_branchmergeproposallisting.py 2009-07-17 00:26:05 +0000
+++ lib/lp/code/browser/tests/test_branchmergeproposallisting.py 2009-08-02 23:25:37 +0000
@@ -8,14 +8,15 @@
8from unittest import TestLoader8from unittest import TestLoader
99
10import transaction10import transaction
11from zope.security.proxy import removeSecurityProxy
1112
13from canonical.launchpad.webapp.servers import LaunchpadTestRequest
14from canonical.testing import DatabaseFunctionalLayer
12from lp.code.browser.branchmergeproposallisting import (15from lp.code.browser.branchmergeproposallisting import (
13 ActiveReviewsView, BranchMergeProposalListingView, PersonActiveReviewsView,16 ActiveReviewsView, BranchMergeProposalListingView, PersonActiveReviewsView,
14 ProductActiveReviewsView)17 ProductActiveReviewsView)
15from lp.code.enums import CodeReviewVote18from lp.code.enums import CodeReviewVote
16from lp.testing import ANONYMOUS, login, login_person, TestCaseWithFactory19from lp.testing import ANONYMOUS, login, login_person, TestCaseWithFactory
17from canonical.launchpad.webapp.servers import LaunchpadTestRequest
18from canonical.testing import DatabaseFunctionalLayer
1920
20_default = object()21_default = object()
2122
@@ -201,8 +202,7 @@
201 self.assertReviewGroupForUser(202 self.assertReviewGroupForUser(
202 self.bmp.registrant, self._view.OTHER)203 self.bmp.registrant, self._view.OTHER)
203 team = self.factory.makeTeam(self.bmp.registrant)204 team = self.factory.makeTeam(self.bmp.registrant)
204 login_person(self.bmp.source_branch.owner)205 removeSecurityProxy(self.bmp.source_branch).owner = team
205 self.bmp.source_branch.owner = team
206 self.assertReviewGroupForUser(206 self.assertReviewGroupForUser(
207 self.bmp.registrant, ActiveReviewsView.MINE)207 self.bmp.registrant, ActiveReviewsView.MINE)
208208
209209
=== modified file 'lib/lp/code/configure.zcml'
--- lib/lp/code/configure.zcml 2009-07-23 02:06:55 +0000
+++ lib/lp/code/configure.zcml 2009-07-30 00:30:02 +0000
@@ -430,9 +430,9 @@
430 "/>430 "/>
431 <require431 <require
432 permission="launchpad.Edit"432 permission="launchpad.Edit"
433 attributes="destroySelf setPrivate"433 attributes="destroySelf setPrivate setOwner setTarget"
434 set_attributes="name url mirror_status_message434 set_attributes="name url mirror_status_message
435 owner author description product lifecycle_status435 description lifecycle_status
436 last_mirrored last_mirrored_id last_mirror_attempt436 last_mirrored last_mirrored_id last_mirror_attempt
437 mirror_failures pull_disabled next_mirror_time437 mirror_failures pull_disabled next_mirror_time
438 last_scanned last_scanned_id revision_count branch_type438 last_scanned last_scanned_id revision_count branch_type
439439
=== modified file 'lib/lp/code/doc/branch.txt'
--- lib/lp/code/doc/branch.txt 2009-06-16 03:31:05 +0000
+++ lib/lp/code/doc/branch.txt 2009-08-02 23:59:31 +0000
@@ -116,9 +116,9 @@
116 >>> print new_branch.owner.name116 >>> print new_branch.owner.name
117 registrant117 registrant
118118
119A user can create a branch where the owner is either themselves,119A user can create a branch where the owner is either themselves, or a team
120or a team that they are a member of. The registrant is not writable,120that they are a member of. Neither the owner nor the registrant are writable,
121whereas the owner is.121but the owner can be set using the `setOwner` method.
122122
123 >>> login('admin@canonical.com')123 >>> login('admin@canonical.com')
124 >>> new_branch.registrant = factory.makePerson()124 >>> new_branch.registrant = factory.makePerson()
@@ -126,7 +126,8 @@
126 ...126 ...
127 ForbiddenAttribute: ('registrant', <Branch ...>)127 ForbiddenAttribute: ('registrant', <Branch ...>)
128128
129 >>> new_branch.owner = factory.makePerson(name='new-owner')129 >>> team = factory.makeTeam(name='new-owner', owner=new_branch.owner)
130 >>> new_branch.setOwner(new_owner=team, user=new_branch.owner)
130 >>> print new_branch.registrant.name131 >>> print new_branch.registrant.name
131 registrant132 registrant
132 >>> print new_branch.owner.name133 >>> print new_branch.owner.name
133134
=== modified file 'lib/lp/code/interfaces/branch.py'
--- lib/lp/code/interfaces/branch.py 2009-07-29 03:44:05 +0000
+++ lib/lp/code/interfaces/branch.py 2009-08-05 04:02:58 +0000
@@ -19,6 +19,7 @@
19 'BranchCreatorNotMemberOfOwnerTeam',19 'BranchCreatorNotMemberOfOwnerTeam',
20 'BranchCreatorNotOwner',20 'BranchCreatorNotOwner',
21 'BranchExists',21 'BranchExists',
22 'BranchTargetError',
22 'BranchTypeError',23 'BranchTypeError',
23 'CannotDeleteBranch',24 'CannotDeleteBranch',
24 'DEFAULT_BRANCH_STATUS_IN_LISTING',25 'DEFAULT_BRANCH_STATUS_IN_LISTING',
@@ -90,7 +91,7 @@
90 """Raised when creating a branch that already exists."""91 """Raised when creating a branch that already exists."""
9192
92 def __init__(self, existing_branch):93 def __init__(self, existing_branch):
93 # XXX: JonathanLange 2008-12-04 spec=package-branches: This error94 # XXX: TimPenhey 2009-07-12 bug=405214: This error
94 # message logic is incorrect, but the exact text is being tested95 # message logic is incorrect, but the exact text is being tested
95 # in branch-xmlrpc.txt.96 # in branch-xmlrpc.txt.
96 params = {'name': existing_branch.name}97 params = {'name': existing_branch.name}
@@ -108,6 +109,10 @@
108 BranchCreationException.__init__(self, message)109 BranchCreationException.__init__(self, message)
109110
110111
112class BranchTargetError(Exception):
113 """Raised when there is an error determining a branch target."""
114
115
111class CannotDeleteBranch(Exception):116class CannotDeleteBranch(Exception):
112 """The branch cannot be deleted at this time."""117 """The branch cannot be deleted at this time."""
113118
@@ -402,14 +407,45 @@
402 title=_("The user that registered the branch."),407 title=_("The user that registered the branch."),
403 required=True, readonly=True,408 required=True, readonly=True,
404 vocabulary='ValidPersonOrTeam'))409 vocabulary='ValidPersonOrTeam'))
410
405 owner = exported(411 owner = exported(
406 ParticipatingPersonChoice(412 ParticipatingPersonChoice(
407 title=_('Owner'),413 title=_('Owner'),
408 required=True,414 required=True, readonly=True,
409 vocabulary='UserTeamsParticipationPlusSelf',415 vocabulary='UserTeamsParticipationPlusSelf',
410 description=_("Either yourself or a team you are a member of. "416 description=_("Either yourself or a team you are a member of. "
411 "This controls who can modify the branch.")))417 "This controls who can modify the branch.")))
412418
419 @call_with(user=REQUEST_USER)
420 @operation_parameters(
421 new_owner=Reference(
422 title=_("The new owner of the branch."),
423 schema=IPerson))
424 @export_write_operation()
425 def setOwner(new_owner, user):
426 """Set the owner of the branch to be `new_owner`."""
427
428 @call_with(user=REQUEST_USER)
429 @operation_parameters(
430 project=Reference(
431 title=_("The project the branch belongs to."),
432 schema=Interface, required=False), # Really IProduct
433 source_package=Reference(
434 title=_("The source package the branch belongs to."),
435 schema=Interface, required=False)) # Really ISourcePackage
436 @export_write_operation()
437 def setTarget(user, project=None, source_package=None):
438 """Set the target of the branch to be `project` or `source_package`.
439
440 Only one of `project` or `source_package` can be set, and if neither
441 is set, the branch gets moved into the junk namespace of the branch
442 owner.
443
444 :raise: `BranchTargetError` if both project and source_package are set,
445 or if either the project or source_package fail to be adapted to an
446 IBranchTarget.
447 """
448
413 reviewer = exported(449 reviewer = exported(
414 PublicPersonChoice(450 PublicPersonChoice(
415 title=_('Default Review Team'),451 title=_('Default Review Team'),
@@ -456,7 +492,7 @@
456 product = exported(492 product = exported(
457 ReferenceChoice(493 ReferenceChoice(
458 title=_('Project'),494 title=_('Project'),
459 required=False,495 required=False, readonly=True,
460 vocabulary='Product',496 vocabulary='Product',
461 schema=Interface,497 schema=Interface,
462 description=_("The project this branch belongs to.")),498 description=_("The project this branch belongs to.")),
463499
=== modified file 'lib/lp/code/interfaces/branchnamespace.py'
--- lib/lp/code/interfaces/branchnamespace.py 2009-06-25 04:06:00 +0000
+++ lib/lp/code/interfaces/branchnamespace.py 2009-07-27 10:19:21 +0000
@@ -70,6 +70,24 @@
70 def isNameUsed(name):70 def isNameUsed(name):
71 """Is 'name' already used in this namespace?"""71 """Is 'name' already used in this namespace?"""
7272
73 def moveBranch(branch, mover, new_name=None, rename_if_necessary=False):
74 """Move the branch into this namespace.
75
76 :param branch: The `IBranch` to move.
77 :param mover: The `IPerson` doing the moving.
78 :param new_name: A new name for the branch.
79 :param rename_if_necessary: Rename the branch if the branch name
80 exists already in this namespace.
81 :raises BranchCreatorNotMemberOfOwnerTeam: if the namespace owner is
82 a team, and 'mover' is not in that team.
83 :raises BranchCreatorNotOwner: if the namespace owner is an individual
84 and 'mover' is not the owner.
85 :raises BranchCreationForbidden: if 'mover' is not allowed to create
86 a branch in this namespace due to privacy rules.
87 :raises BranchExists: if a branch with the 'name' exists already in
88 the namespace, and 'rename_if_necessary' is False.
89 """
90
7391
74class IBranchNamespacePolicy(Interface):92class IBranchNamespacePolicy(Interface):
75 """Methods relating to branch creation and validation."""93 """Methods relating to branch creation and validation."""
@@ -133,11 +151,13 @@
133 validation constraints on IBranch.name.151 validation constraints on IBranch.name.
134 """152 """
135153
136 def validateMove(branch, mover):154 def validateMove(branch, mover, name=None):
137 """Check that 'mover' can move 'branch' into this namespace.155 """Check that 'mover' can move 'branch' into this namespace.
138156
139 :param branch: An `IBranch` that might be moved.157 :param branch: An `IBranch` that might be moved.
140 :param mover: The `IPerson` who would move it.158 :param mover: The `IPerson` who would move it.
159 :param name: A new name for the branch. If None, the branch name is
160 used.
141 :raises BranchCreatorNotMemberOfOwnerTeam: if the namespace owner is161 :raises BranchCreatorNotMemberOfOwnerTeam: if the namespace owner is
142 a team, and 'mover' is not in that team.162 a team, and 'mover' is not in that team.
143 :raises BranchCreatorNotOwner: if the namespace owner is an individual163 :raises BranchCreatorNotOwner: if the namespace owner is an individual
144164
=== modified file 'lib/lp/code/interfaces/diff.py'
--- lib/lp/code/interfaces/diff.py 2009-06-25 04:06:00 +0000
+++ lib/lp/code/interfaces/diff.py 2009-08-04 23:07:33 +0000
@@ -27,7 +27,15 @@
27class IDiff(Interface):27class IDiff(Interface):
28 """A diff that is stored in the Library."""28 """A diff that is stored in the Library."""
2929
30 text = Text(title=_('Textual contents of a diff.'), readonly=True)30 text = Text(
31 title=_('Textual contents of a diff.'), readonly=True,
32 description=_("The text may be cut off at a defined maximum size."))
33
34 oversized = Bool(
35 readonly=True,
36 description=_(
37 "True if the size of the content is over the defined maximum "
38 "size."))
3139
32 diff_text = exported(40 diff_text = exported(
33 Bytes(title=_('Content of this diff'), required=True, readonly=True))41 Bytes(title=_('Content of this diff'), required=True, readonly=True))
3442
=== modified file 'lib/lp/code/mail/branchmergeproposal.py'
--- lib/lp/code/mail/branchmergeproposal.py 2009-07-22 18:37:32 +0000
+++ lib/lp/code/mail/branchmergeproposal.py 2009-08-05 02:18:13 +0000
@@ -193,6 +193,7 @@
193 'gap': '',193 'gap': '',
194 'reviews': '',194 'reviews': '',
195 'whiteboard': '', # No more whiteboard.195 'whiteboard': '', # No more whiteboard.
196 'diff_cutoff_warning': '',
196 }197 }
197198
198 requested_reviews = []199 requested_reviews = []
@@ -212,6 +213,12 @@
212 params['comment'] = (self.comment.message.text_contents)213 params['comment'] = (self.comment.message.text_contents)
213 if len(requested_reviews) > 0:214 if len(requested_reviews) > 0:
214 params['gap'] = '\n\n'215 params['gap'] = '\n\n'
216
217 if (self.review_diff is not None and
218 self.review_diff.diff.oversized):
219 params['diff_cutoff_warning'] = (
220 "The attached diff has been truncated due to its size.")
221
215 return params222 return params
216223
217 def _getTemplateParams(self, email):224 def _getTemplateParams(self, email):
218225
=== modified file 'lib/lp/code/mail/tests/test_branchmergeproposal.py'
--- lib/lp/code/mail/tests/test_branchmergeproposal.py 2009-07-22 18:37:32 +0000
+++ lib/lp/code/mail/tests/test_branchmergeproposal.py 2009-08-05 02:25:16 +0000
@@ -3,7 +3,7 @@
33
4"""Tests for BranchMergeProposal mailings"""4"""Tests for BranchMergeProposal mailings"""
55
6from unittest import TestLoader, TestCase6from unittest import TestLoader
7import transaction7import transaction
88
9from zope.security.proxy import removeSecurityProxy9from zope.security.proxy import removeSecurityProxy
@@ -22,20 +22,17 @@
22from lp.code.model.branch import update_trigger_modified_fields22from lp.code.model.branch import update_trigger_modified_fields
23from lp.code.model.codereviewvote import CodeReviewVoteReference23from lp.code.model.codereviewvote import CodeReviewVoteReference
24from canonical.launchpad.webapp import canonical_url24from canonical.launchpad.webapp import canonical_url
25from lp.testing import login, login_person, TestCaseWithFactory25from lp.testing import login_person, TestCaseWithFactory
26from lp.testing.factory import LaunchpadObjectFactory
27from lp.testing.mail_helpers import pop_notifications26from lp.testing.mail_helpers import pop_notifications
2827
2928
30class TestMergeProposalMailing(TestCase):29class TestMergeProposalMailing(TestCaseWithFactory):
31 """Test that reasonable mailings are generated"""30 """Test that reasonable mailings are generated"""
3231
33 layer = LaunchpadFunctionalLayer32 layer = LaunchpadFunctionalLayer
3433
35 def setUp(self):34 def setUp(self):
36 TestCase.setUp(self)35 super(TestMergeProposalMailing, self).setUp('admin@canonical.com')
37 login('admin@canonical.com')
38 self.factory = LaunchpadObjectFactory()
3936
40 def makeProposalWithSubscriber(self, diff_text=None,37 def makeProposalWithSubscriber(self, diff_text=None,
41 initial_comment=None):38 initial_comment=None):
@@ -182,6 +179,22 @@
182 attachment['Content-Disposition'])179 attachment['Content-Disposition'])
183 self.assertEqual('Fake diff', attachment.get_payload(decode=True))180 self.assertEqual('Fake diff', attachment.get_payload(decode=True))
184181
182 def test_generateEmail_attaches_diff_oversize_truncated(self):
183 """An oversized diff will be truncated, and the receiver informed."""
184 self.pushConfig("diff", max_read_size=25)
185 content = "1234567890" * 10
186 bmp, subscriber = self.makeProposalWithSubscriber(
187 diff_text=content)
188 mailer = BMPMailer.forCreation(bmp, bmp.registrant)
189 ctrl = mailer.generateEmail('baz.quxx@example.com', subscriber)
190 (attachment,) = ctrl.attachments
191 self.assertEqual('text/x-diff', attachment['Content-Type'])
192 self.assertEqual('inline; filename="review-diff.txt"',
193 attachment['Content-Disposition'])
194 self.assertEqual(content[:25], attachment.get_payload(decode=True))
195 warning_text = "The attached diff has been truncated due to its size."
196 self.assertTrue(warning_text in ctrl.body)
197
185 def test_forModificationNoModification(self):198 def test_forModificationNoModification(self):
186 """Ensure None is returned if no change has been made."""199 """Ensure None is returned if no change has been made."""
187 merge_proposal, person = self.makeProposalWithSubscriber()200 merge_proposal, person = self.makeProposalWithSubscriber()
188201
=== modified file 'lib/lp/code/model/branch.py'
--- lib/lp/code/model/branch.py 2009-07-19 04:41:14 +0000
+++ lib/lp/code/model/branch.py 2009-07-31 01:07:04 +0000
@@ -57,7 +57,7 @@
57from lp.code.event.branchmergeproposal import NewBranchMergeProposalEvent57from lp.code.event.branchmergeproposal import NewBranchMergeProposalEvent
58from lp.code.interfaces.branch import (58from lp.code.interfaces.branch import (
59 bazaar_identity, BranchCannotBePrivate, BranchCannotBePublic,59 bazaar_identity, BranchCannotBePrivate, BranchCannotBePublic,
60 BranchTypeError, CannotDeleteBranch,60 BranchTargetError, BranchTypeError, CannotDeleteBranch,
61 DEFAULT_BRANCH_STATUS_IN_LISTING, IBranch,61 DEFAULT_BRANCH_STATUS_IN_LISTING, IBranch,
62 IBranchNavigationMenu, IBranchSet)62 IBranchNavigationMenu, IBranchSet)
63from lp.code.interfaces.branchcollection import IAllBranches63from lp.code.interfaces.branchcollection import IAllBranches
@@ -71,7 +71,7 @@
71from lp.code.interfaces.seriessourcepackagebranch import (71from lp.code.interfaces.seriessourcepackagebranch import (
72 IFindOfficialBranchLinks)72 IFindOfficialBranchLinks)
73from lp.registry.interfaces.person import (73from lp.registry.interfaces.person import (
74 validate_person_not_private_membership, validate_public_person)74 IPerson, validate_person_not_private_membership, validate_public_person)
7575
7676
77class Branch(SQLBase):77class Branch(SQLBase):
@@ -113,6 +113,12 @@
113 owner = ForeignKey(113 owner = ForeignKey(
114 dbName='owner', foreignKey='Person',114 dbName='owner', foreignKey='Person',
115 storm_validator=validate_person_not_private_membership, notNull=True)115 storm_validator=validate_person_not_private_membership, notNull=True)
116
117 def setOwner(self, new_owner, user):
118 """See `IBranch`."""
119 new_namespace = self.target.getNamespace(new_owner)
120 new_namespace.moveBranch(self, user, rename_if_necessary=True)
121
116 reviewer = ForeignKey(122 reviewer = ForeignKey(
117 dbName='reviewer', foreignKey='Person',123 dbName='reviewer', foreignKey='Person',
118 storm_validator=validate_public_person, default=None)124 storm_validator=validate_public_person, default=None)
@@ -162,6 +168,28 @@
162 target = self.product168 target = self.product
163 return IBranchTarget(target)169 return IBranchTarget(target)
164170
171 def setTarget(self, user, project=None, source_package=None):
172 """See `IBranch`."""
173 if project is not None:
174 if source_package is not None:
175 raise BranchTargetError(
176 'Cannot specify both a project and a source package.')
177 else:
178 target = IBranchTarget(project)
179 if target is None:
180 raise BranchTargetError(
181 '%r is not a valid project target' % project)
182 elif source_package is not None:
183 target = IBranchTarget(source_package)
184 if target is None:
185 raise BranchTargetError(
186 '%r is not a valid source package target' % source_package)
187 else:
188 target = IBranchTarget(self.owner)
189 # Person targets are always valid.
190 namespace = target.getNamespace(self.owner)
191 namespace.moveBranch(self, user, rename_if_necessary=True)
192
165 @property193 @property
166 def namespace(self):194 def namespace(self):
167 """See `IBranch`."""195 """See `IBranch`."""
168196
=== modified file 'lib/lp/code/model/branchnamespace.py'
--- lib/lp/code/model/branchnamespace.py 2009-06-25 04:06:00 +0000
+++ lib/lp/code/model/branchnamespace.py 2009-08-04 00:41:49 +0000
@@ -16,6 +16,7 @@
16from zope.component import getUtility16from zope.component import getUtility
17from zope.event import notify17from zope.event import notify
18from zope.interface import implements18from zope.interface import implements
19from zope.security.proxy import removeSecurityProxy
1920
20from lazr.lifecycle.event import ObjectCreatedEvent21from lazr.lifecycle.event import ObjectCreatedEvent
21from storm.locals import And22from storm.locals import And
@@ -160,6 +161,25 @@
160 self.validateBranchName(name)161 self.validateBranchName(name)
161 self.validateRegistrant(mover)162 self.validateRegistrant(mover)
162163
164 def moveBranch(self, branch, mover, new_name=None,
165 rename_if_necessary=False):
166 """See `IBranchNamespace`."""
167 # Check to see if the branch is already in this namespace.
168 old_namespace = branch.namespace
169 if self.name == old_namespace.name:
170 return
171 if new_name is None:
172 new_name = branch.name
173 if rename_if_necessary:
174 new_name = self.findUnusedName(new_name)
175 self.validateMove(branch, mover, new_name)
176 # Remove the security proxy of the branch as the owner and target
177 # attributes are readonly through the interface.
178 naked_branch = removeSecurityProxy(branch)
179 naked_branch.owner = self.owner
180 self.target._retargetBranch(naked_branch)
181 naked_branch.name = new_name
182
163 def createBranchWithPrefix(self, branch_type, prefix, registrant,183 def createBranchWithPrefix(self, branch_type, prefix, registrant,
164 url=None):184 url=None):
165 """See `IBranchNamespace`."""185 """See `IBranchNamespace`."""
166186
=== modified file 'lib/lp/code/model/branchtarget.py'
--- lib/lp/code/model/branchtarget.py 2009-07-17 00:26:05 +0000
+++ lib/lp/code/model/branchtarget.py 2009-08-04 00:41:49 +0000
@@ -13,7 +13,8 @@
1313
14from zope.component import getUtility14from zope.component import getUtility
15from zope.interface import implements15from zope.interface import implements
16from zope.security.proxy import isinstance as zope_isinstance16from zope.security.proxy import (
17 removeSecurityProxy, isinstance as zope_isinstance)
1718
18from lp.code.interfaces.branchcollection import IAllBranches19from lp.code.interfaces.branchcollection import IAllBranches
19from lp.code.interfaces.branchtarget import (20from lp.code.interfaces.branchtarget import (
@@ -128,6 +129,16 @@
128 # those cases.129 # those cases.
129 return bug.default_bugtask130 return bug.default_bugtask
130131
132 def _retargetBranch(self, branch):
133 """Set the branch target to refer to this target.
134
135 This only updates the target related attributes of the branch, and
136 expects a branch without a security proxy as a parameter.
137 """
138 branch.product = None
139 branch.distroseries = self.sourcepackage.distroseries
140 branch.sourcepackagename = self.sourcepackage.sourcepackagename
141
131142
132class PersonBranchTarget(_BaseBranchTarget):143class PersonBranchTarget(_BaseBranchTarget):
133 implements(IBranchTarget)144 implements(IBranchTarget)
@@ -182,6 +193,16 @@
182 """See `IBranchTarget`."""193 """See `IBranchTarget`."""
183 return bug.default_bugtask194 return bug.default_bugtask
184195
196 def _retargetBranch(self, branch):
197 """Set the branch target to refer to this target.
198
199 This only updates the target related attributes of the branch, and
200 expects a branch without a security proxy as a parameter.
201 """
202 branch.product = None
203 branch.distroseries = None
204 branch.sourcepackagename = None
205
185206
186class ProductBranchTarget(_BaseBranchTarget):207class ProductBranchTarget(_BaseBranchTarget):
187 implements(IBranchTarget)208 implements(IBranchTarget)
@@ -265,6 +286,16 @@
265 task = bug.bugtasks[0]286 task = bug.bugtasks[0]
266 return task287 return task
267288
289 def _retargetBranch(self, branch):
290 """Set the branch target to refer to this target.
291
292 This only updates the target related attributes of the branch, and
293 expects a branch without a security proxy as a parameter.
294 """
295 branch.product = self.product
296 branch.distroseries = None
297 branch.sourcepackagename = None
298
268299
269def get_canonical_url_data_for_target(branch_target):300def get_canonical_url_data_for_target(branch_target):
270 """Return the `ICanonicalUrlData` for an `IBranchTarget`."""301 """Return the `ICanonicalUrlData` for an `IBranchTarget`."""
271302
=== modified file 'lib/lp/code/model/diff.py'
--- lib/lp/code/model/diff.py 2009-06-25 04:06:00 +0000
+++ lib/lp/code/model/diff.py 2009-08-04 23:07:33 +0000
@@ -15,8 +15,9 @@
15from zope.component import getUtility15from zope.component import getUtility
16from zope.interface import classProvides, implements16from zope.interface import classProvides, implements
1717
18from canonical.config import config
19from canonical.database.sqlbase import SQLBase
18from canonical.uuid import generate_uuid20from canonical.uuid import generate_uuid
19from canonical.database.sqlbase import SQLBase
2021
21from lp.code.interfaces.diff import (22from lp.code.interfaces.diff import (
22 IDiff, IPreviewDiff, IStaticDiff, IStaticDiffSource)23 IDiff, IPreviewDiff, IStaticDiff, IStaticDiffSource)
@@ -45,10 +46,19 @@
45 else:46 else:
46 self.diff_text.open()47 self.diff_text.open()
47 try:48 try:
48 return self.diff_text.read()49 return self.diff_text.read(config.diff.max_read_size)
49 finally:50 finally:
50 self.diff_text.close()51 self.diff_text.close()
5152
53 @property
54 def oversized(self):
55 # If the size of the content of the librarian file is over the
56 # config.diff.max_read_size, then we have an oversized diff.
57 if self.diff_text is None:
58 return False
59 diff_size = self.diff_text.content.filesize
60 return diff_size > config.diff.max_read_size
61
52 @classmethod62 @classmethod
53 def fromTrees(klass, from_tree, to_tree, filename=None):63 def fromTrees(klass, from_tree, to_tree, filename=None):
54 """Create a Diff from two Bazaar trees.64 """Create a Diff from two Bazaar trees.
5565
=== modified file 'lib/lp/code/model/tests/test_branch.py'
--- lib/lp/code/model/tests/test_branch.py 2009-07-23 02:06:55 +0000
+++ lib/lp/code/model/tests/test_branch.py 2009-08-05 02:04:06 +0000
@@ -39,7 +39,8 @@
39 BranchVisibilityRule, CodeReviewNotificationLevel)39 BranchVisibilityRule, CodeReviewNotificationLevel)
40from lp.code.interfaces.branch import (40from lp.code.interfaces.branch import (
41 BranchCannotBePrivate, BranchCannotBePublic,41 BranchCannotBePrivate, BranchCannotBePublic,
42 CannotDeleteBranch, DEFAULT_BRANCH_STATUS_IN_LISTING)42 BranchCreatorNotMemberOfOwnerTeam, BranchCreatorNotOwner,
43 BranchTargetError, CannotDeleteBranch, DEFAULT_BRANCH_STATUS_IN_LISTING)
43from lp.code.interfaces.branchlookup import IBranchLookup44from lp.code.interfaces.branchlookup import IBranchLookup
44from lp.code.interfaces.branchnamespace import IBranchNamespaceSet45from lp.code.interfaces.branchnamespace import IBranchNamespaceSet
45from lp.code.interfaces.branchmergeproposal import InvalidBranchMergeProposal46from lp.code.interfaces.branchmergeproposal import InvalidBranchMergeProposal
@@ -57,7 +58,6 @@
57from lp.code.model.codeimport import CodeImport, CodeImportSet58from lp.code.model.codeimport import CodeImport, CodeImportSet
58from lp.code.model.codereviewcomment import CodeReviewComment59from lp.code.model.codereviewcomment import CodeReviewComment
59from lp.registry.interfaces.person import IPersonSet60from lp.registry.interfaces.person import IPersonSet
60from lp.registry.interfaces.product import IProductSet
61from lp.registry.model.product import ProductSet61from lp.registry.model.product import ProductSet
62from lp.registry.model.sourcepackage import SourcePackage62from lp.registry.model.sourcepackage import SourcePackage
63from lp.soyuz.interfaces.publishing import PackagePublishingPocket63from lp.soyuz.interfaces.publishing import PackagePublishingPocket
@@ -213,8 +213,7 @@
213 # attribute is updated too.213 # attribute is updated too.
214 branch = self.factory.makeAnyBranch()214 branch = self.factory.makeAnyBranch()
215 new_owner = self.factory.makePerson()215 new_owner = self.factory.makePerson()
216 login('admin@canonical.com')216 removeSecurityProxy(branch).owner = new_owner
217 branch.owner = new_owner
218 # Call the function that is normally called through the event system217 # Call the function that is normally called through the event system
219 # to auto reload the fields updated by the db triggers.218 # to auto reload the fields updated by the db triggers.
220 update_trigger_modified_fields(branch)219 update_trigger_modified_fields(branch)
@@ -1054,9 +1053,9 @@
10541053
1055 def setUp(self):1054 def setUp(self):
1056 TestCaseWithFactory.setUp(self, 'admin@canonical.com')1055 TestCaseWithFactory.setUp(self, 'admin@canonical.com')
1057 self.product = getUtility(IProductSet).getByName('firefox')1056 self.product = self.factory.makeProduct()
10581057
1059 self.user = getUtility(IPersonSet).getByName('no-priv')1058 self.user = self.factory.makePerson()
1060 self.source = self.factory.makeProductBranch(1059 self.source = self.factory.makeProductBranch(
1061 name='source-branch', owner=self.user, product=self.product)1060 name='source-branch', owner=self.user, product=self.product)
1062 self.target = self.factory.makeProductBranch(1061 self.target = self.factory.makeProductBranch(
@@ -1069,7 +1068,7 @@
10691068
1070 def test_junkSource(self):1069 def test_junkSource(self):
1071 """Junk branches cannot be used as a source for merge proposals."""1070 """Junk branches cannot be used as a source for merge proposals."""
1072 self.source.product = None1071 self.source.setTarget(user=self.source.owner)
1073 self.assertRaises(1072 self.assertRaises(
1074 InvalidBranchMergeProposal, self.source.addLandingTarget,1073 InvalidBranchMergeProposal, self.source.addLandingTarget,
1075 self.user, self.target)1074 self.user, self.target)
@@ -1078,12 +1077,13 @@
1078 """The product of the target branch must match the product of the1077 """The product of the target branch must match the product of the
1079 source branch.1078 source branch.
1080 """1079 """
1081 self.target.product = None1080 self.target.setTarget(user=self.target.owner)
1082 self.assertRaises(1081 self.assertRaises(
1083 InvalidBranchMergeProposal, self.source.addLandingTarget,1082 InvalidBranchMergeProposal, self.source.addLandingTarget,
1084 self.user, self.target)1083 self.user, self.target)
10851084
1086 self.target.product = getUtility(IProductSet).getByName('bzr')1085 project = self.factory.makeProduct()
1086 self.target.setTarget(user=self.target.owner, project=project)
1087 self.assertRaises(1087 self.assertRaises(
1088 InvalidBranchMergeProposal, self.source.addLandingTarget,1088 InvalidBranchMergeProposal, self.source.addLandingTarget,
1089 self.user, self.target)1089 self.user, self.target)
@@ -1097,12 +1097,13 @@
1097 def test_dependentBranchSameProduct(self):1097 def test_dependentBranchSameProduct(self):
1098 """The dependent branch, if it is there, must be for the same product.1098 """The dependent branch, if it is there, must be for the same product.
1099 """1099 """
1100 self.dependent.product = None1100 self.dependent.setTarget(user=self.dependent.owner)
1101 self.assertRaises(1101 self.assertRaises(
1102 InvalidBranchMergeProposal, self.source.addLandingTarget,1102 InvalidBranchMergeProposal, self.source.addLandingTarget,
1103 self.user, self.target, self.dependent)1103 self.user, self.target, self.dependent)
11041104
1105 self.dependent.product = getUtility(IProductSet).getByName('bzr')1105 project = self.factory.makeProduct()
1106 self.dependent.setTarget(user=self.dependent.owner, project=project)
1106 self.assertRaises(1107 self.assertRaises(
1107 InvalidBranchMergeProposal, self.source.addLandingTarget,1108 InvalidBranchMergeProposal, self.source.addLandingTarget,
1108 self.user, self.target, self.dependent)1109 self.user, self.target, self.dependent)
@@ -1650,5 +1651,149 @@
1650 self.assertEqual(branch.spec_links.count(), 0)1651 self.assertEqual(branch.spec_links.count(), 0)
16511652
16521653
1654class TestBranchSetOwner(TestCaseWithFactory):
1655 """Tests for IBranch.setOwner."""
1656
1657 layer = DatabaseFunctionalLayer
1658
1659 def test_owner_sets_team(self):
1660 # The owner of the branch can set the owner of the branch to be a team
1661 # they are a member of.
1662 branch = self.factory.makeAnyBranch()
1663 team = self.factory.makeTeam(owner=branch.owner)
1664 login_person(branch.owner)
1665 branch.setOwner(team, branch.owner)
1666 self.assertEqual(team, branch.owner)
1667
1668 def test_owner_cannot_set_nonmember_team(self):
1669 # The owner of the branch cannot set the owner to be a team they are
1670 # not a member of.
1671 branch = self.factory.makeAnyBranch()
1672 team = self.factory.makeTeam()
1673 login_person(branch.owner)
1674 self.assertRaises(
1675 BranchCreatorNotMemberOfOwnerTeam,
1676 branch.setOwner,
1677 team, branch.owner)
1678
1679 def test_owner_cannot_set_other_user(self):
1680 # The owner of the branch cannot set the new owner to be another
1681 # person.
1682 branch = self.factory.makeAnyBranch()
1683 person = self.factory.makePerson()
1684 login_person(branch.owner)
1685 self.assertRaises(
1686 BranchCreatorNotOwner,
1687 branch.setOwner,
1688 person, branch.owner)
1689
1690 def test_admin_can_set_any_team_or_person(self):
1691 # A Launchpad admin can set the branch to be owned by any team or
1692 # person.
1693 branch = self.factory.makeAnyBranch()
1694 team = self.factory.makeTeam()
1695 # To get a random administrator, choose the admin team owner.
1696 admin = getUtility(ILaunchpadCelebrities).admin.teamowner
1697 login_person(admin)
1698 branch.setOwner(team, admin)
1699 self.assertEqual(team, branch.owner)
1700 person = self.factory.makePerson()
1701 branch.setOwner(person, admin)
1702 self.assertEqual(person, branch.owner)
1703
1704 def test_bazaar_experts_can_set_any_team_or_person(self):
1705 # A bazaar expert can set the branch to be owned by any team or
1706 # person.
1707 branch = self.factory.makeAnyBranch()
1708 team = self.factory.makeTeam()
1709 # To get a random administrator, choose the admin team owner.
1710 experts = getUtility(ILaunchpadCelebrities).bazaar_experts.teamowner
1711 login_person(experts)
1712 branch.setOwner(team, experts)
1713 self.assertEqual(team, branch.owner)
1714 person = self.factory.makePerson()
1715 branch.setOwner(person, experts)
1716 self.assertEqual(person, branch.owner)
1717
1718
1719class TestBranchSetTarget(TestCaseWithFactory):
1720 """Tests for IBranch.setTarget."""
1721
1722 layer = DatabaseFunctionalLayer
1723
1724 def test_not_both_project_and_source_package(self):
1725 # Only one of project or source_package can be passed in, not both.
1726 branch = self.factory.makePersonalBranch()
1727 project = self.factory.makeProduct()
1728 source_package = self.factory.makeSourcePackage()
1729 login_person(branch.owner)
1730 self.assertRaises(
1731 BranchTargetError,
1732 branch.setTarget,
1733 user=branch.owner, project=project, source_package=source_package)
1734
1735 def test_junk_branch_to_project_branch(self):
1736 # A junk branch can be moved to a project.
1737 branch = self.factory.makePersonalBranch()
1738 project = self.factory.makeProduct()
1739 login_person(branch.owner)
1740 branch.setTarget(user=branch.owner, project=project)
1741 self.assertEqual(project, branch.target.context)
1742
1743 def test_junk_branch_to_package_branch(self):
1744 # A junk branch can be moved to a source package.
1745 branch = self.factory.makePersonalBranch()
1746 source_package = self.factory.makeSourcePackage()
1747 login_person(branch.owner)
1748 branch.setTarget(user=branch.owner, source_package=source_package)
1749 self.assertEqual(source_package, branch.target.context)
1750
1751 def test_project_branch_to_other_project_branch(self):
1752 # Move a branch from one project to another.
1753 branch = self.factory.makeProductBranch()
1754 project = self.factory.makeProduct()
1755 login_person(branch.owner)
1756 branch.setTarget(user=branch.owner, project=project)
1757 self.assertEqual(project, branch.target.context)
1758
1759 def test_project_branch_to_package_branch(self):
1760 # Move a branch from a project to a package.
1761 branch = self.factory.makeProductBranch()
1762 source_package = self.factory.makeSourcePackage()
1763 login_person(branch.owner)
1764 branch.setTarget(user=branch.owner, source_package=source_package)
1765 self.assertEqual(source_package, branch.target.context)
1766
1767 def test_project_branch_to_junk_branch(self):
1768 # Move a branch from a project to junk.
1769 branch = self.factory.makeProductBranch()
1770 login_person(branch.owner)
1771 branch.setTarget(user=branch.owner)
1772 self.assertEqual(branch.owner, branch.target.context)
1773
1774 def test_package_branch_to_other_package_branch(self):
1775 # Move a branch from one package to another.
1776 branch = self.factory.makePackageBranch()
1777 source_package = self.factory.makeSourcePackage()
1778 login_person(branch.owner)
1779 branch.setTarget(user=branch.owner, source_package=source_package)
1780 self.assertEqual(source_package, branch.target.context)
1781
1782 def test_package_branch_to_project_branch(self):
1783 # Move a branch from a package to a project.
1784 branch = self.factory.makePackageBranch()
1785 project = self.factory.makeProduct()
1786 login_person(branch.owner)
1787 branch.setTarget(user=branch.owner, project=project)
1788 self.assertEqual(project, branch.target.context)
1789
1790 def test_package_branch_to_junk_branch(self):
1791 # Move a branch from a package to junk.
1792 branch = self.factory.makePackageBranch()
1793 login_person(branch.owner)
1794 branch.setTarget(user=branch.owner)
1795 self.assertEqual(branch.owner, branch.target.context)
1796
1797
1653def test_suite():1798def test_suite():
1654 return TestLoader().loadTestsFromName(__name__)1799 return TestLoader().loadTestsFromName(__name__)
16551800
=== renamed file 'lib/lp/code/tests/test_branchnamespace.py' => 'lib/lp/code/model/tests/test_branchnamespace.py'
--- lib/lp/code/tests/test_branchnamespace.py 2009-06-25 04:06:00 +0000
+++ lib/lp/code/model/tests/test_branchnamespace.py 2009-07-29 02:16:43 +0000
@@ -1871,5 +1871,70 @@
1871 BranchCreatorNotOwner, self.albert, self.doug)1871 BranchCreatorNotOwner, self.albert, self.doug)
18721872
18731873
1874class TestBranchNamespaceMoveBranch(TestCaseWithFactory):
1875 """Test the IBranchNamespace.moveBranch method.
1876
1877 The edge cases of the validateMove are tested in the NamespaceMixin for
1878 each of the namespaces.
1879 """
1880
1881 layer = DatabaseFunctionalLayer
1882
1883 def assertNamespacesEqual(self, expected, result):
1884 """Assert that the namespaces refer to the same thing.
1885
1886 The name of the namespace contains the user name and the context
1887 parts, so is the easiest thing to check.
1888 """
1889 self.assertEqual(expected.name, result.name)
1890
1891 def test_move_to_same_namespace(self):
1892 # Moving to the same namespace is effectively a no-op. No exceptions
1893 # about matching branch names should be raised.
1894 branch = self.factory.makeAnyBranch()
1895 namespace = branch.namespace
1896 namespace.moveBranch(branch, branch.owner)
1897 self.assertNamespacesEqual(namespace, branch.namespace)
1898
1899 def test_name_clash_raises(self):
1900 # A name clash will raise an exception.
1901 branch = self.factory.makeAnyBranch(name="test")
1902 another = self.factory.makeAnyBranch(owner=branch.owner, name="test")
1903 namespace = another.namespace
1904 self.assertRaises(
1905 BranchExists, namespace.moveBranch, branch, branch.owner)
1906
1907 def test_move_with_rename(self):
1908 # A name clash with 'rename_if_necessary' set to True will cause the
1909 # branch to be renamed instead of raising an error.
1910 branch = self.factory.makeAnyBranch(name="test")
1911 another = self.factory.makeAnyBranch(owner=branch.owner, name="test")
1912 namespace = another.namespace
1913 namespace.moveBranch(branch, branch.owner, rename_if_necessary=True)
1914 self.assertEqual("test-1", branch.name)
1915 self.assertNamespacesEqual(namespace, branch.namespace)
1916
1917 def test_move_with_new_name(self):
1918 # A new name for the branch can be specified as part of the move.
1919 branch = self.factory.makeAnyBranch(name="test")
1920 another = self.factory.makeAnyBranch(owner=branch.owner, name="test")
1921 namespace = another.namespace
1922 namespace.moveBranch(branch, branch.owner, new_name="foo")
1923 self.assertEqual("foo", branch.name)
1924 self.assertNamespacesEqual(namespace, branch.namespace)
1925
1926 def test_sets_branch_owner(self):
1927 # Moving to a new namespace may change the owner of the branch if the
1928 # owner of the namespace is different.
1929 branch = self.factory.makeAnyBranch(name="test")
1930 team = self.factory.makeTeam(branch.owner)
1931 product = self.factory.makeProduct()
1932 namespace = ProductNamespace(team, product)
1933 namespace.moveBranch(branch, branch.owner)
1934 self.assertEqual(team, branch.owner)
1935 # And for paranoia.
1936 self.assertNamespacesEqual(namespace, branch.namespace)
1937
1938
1874def test_suite():1939def test_suite():
1875 return unittest.TestLoader().loadTestsFromName(__name__)1940 return unittest.TestLoader().loadTestsFromName(__name__)
18761941
=== modified file 'lib/lp/code/model/tests/test_branchtarget.py'
--- lib/lp/code/model/tests/test_branchtarget.py 2009-06-25 04:06:00 +0000
+++ lib/lp/code/model/tests/test_branchtarget.py 2009-08-04 00:41:49 +0000
@@ -46,6 +46,24 @@
46 branches = self.target.collection.getBranches()46 branches = self.target.collection.getBranches()
47 self.assertEqual([branch], list(branches))47 self.assertEqual([branch], list(branches))
4848
49 def test_retargetBranch_packageBranch(self):
50 # Retarget an existing package branch to this target.
51 branch = self.factory.makePackageBranch()
52 self.target._retargetBranch(removeSecurityProxy(branch))
53 self.assertEqual(self.target, branch.target)
54
55 def test_retargetBranch_productBranch(self):
56 # Retarget an existing product branch to this target.
57 branch = self.factory.makeProductBranch()
58 self.target._retargetBranch(removeSecurityProxy(branch))
59 self.assertEqual(self.target, branch.target)
60
61 def test_retargetBranch_personalBranch(self):
62 # Retarget an existing personal branch to this target.
63 branch = self.factory.makePersonalBranch()
64 self.target._retargetBranch(removeSecurityProxy(branch))
65 self.assertEqual(self.target, branch.target)
66
4967
50class TestPackageBranchTarget(TestCaseWithFactory, BaseBranchTargetTests):68class TestPackageBranchTarget(TestCaseWithFactory, BaseBranchTargetTests):
5169
@@ -212,6 +230,33 @@
212 # The default merge target is always None.230 # The default merge target is always None.
213 self.assertIs(None, self.target.default_merge_target)231 self.assertIs(None, self.target.default_merge_target)
214232
233 def test_retargetBranch_packageBranch(self):
234 # Retarget an existing package branch to this target. Override the
235 # mixin tests, and specify the owner of the branch. This is needed to
236 # match the target as the target is the branch owner for a personal
237 # branch.
238 branch = self.factory.makePackageBranch(owner=self.original)
239 self.target._retargetBranch(removeSecurityProxy(branch))
240 self.assertEqual(self.target, branch.target)
241
242 def test_retargetBranch_productBranch(self):
243 # Retarget an existing product branch to this target. Override the
244 # mixin tests, and specify the owner of the branch. This is needed to
245 # match the target as the target is the branch owner for a personal
246 # branch.
247 branch = self.factory.makeProductBranch(owner=self.original)
248 self.target._retargetBranch(removeSecurityProxy(branch))
249 self.assertEqual(self.target, branch.target)
250
251 def test_retargetBranch_personalBranch(self):
252 # Retarget an existing personal branch to this target. Override the
253 # mixin tests, and specify the owner of the branch. This is needed to
254 # match the target as the target is the branch owner for a personal
255 # branch.
256 branch = self.factory.makePersonalBranch(owner=self.original)
257 self.target._retargetBranch(removeSecurityProxy(branch))
258 self.assertEqual(self.target, branch.target)
259
215260
216class TestProductBranchTarget(TestCaseWithFactory, BaseBranchTargetTests):261class TestProductBranchTarget(TestCaseWithFactory, BaseBranchTargetTests):
217262
218263
=== modified file 'lib/lp/code/model/tests/test_diff.py'
--- lib/lp/code/model/tests/test_diff.py 2009-06-25 04:06:00 +0000
+++ lib/lp/code/model/tests/test_diff.py 2009-08-04 23:07:33 +0000
@@ -6,27 +6,66 @@
6__metaclass__ = type6__metaclass__ = type
77
88
9from cStringIO import StringIO
9from unittest import TestLoader10from unittest import TestLoader
1011
11from canonical.testing import (
12 DatabaseFunctionalLayer, LaunchpadFunctionalLayer, LaunchpadZopelessLayer)
13import transaction12import transaction
1413
14from canonical.launchpad.webapp import canonical_url
15from canonical.launchpad.webapp.testing import verifyObject
16from canonical.testing import LaunchpadFunctionalLayer, LaunchpadZopelessLayer
15from lp.code.model.diff import Diff, StaticDiff17from lp.code.model.diff import Diff, StaticDiff
16from lp.code.interfaces.diff import (18from lp.code.interfaces.diff import (
17 IDiff, IPreviewDiff, IStaticDiff, IStaticDiffSource)19 IDiff, IPreviewDiff, IStaticDiff, IStaticDiffSource)
18from lp.testing import login, login_person, TestCaseWithFactory20from lp.testing import login, login_person, TestCaseWithFactory
19from canonical.launchpad.webapp import canonical_url
20from canonical.launchpad.webapp.testing import verifyObject
2121
2222
23class TestDiff(TestCaseWithFactory):23class TestDiff(TestCaseWithFactory):
2424
25 layer = DatabaseFunctionalLayer25 layer = LaunchpadFunctionalLayer
2626
27 def test_providesInterface(self):27 def test_providesInterface(self):
28 verifyObject(IDiff, Diff())28 verifyObject(IDiff, Diff())
2929
30 def _create_diff(self, content):
31 # Create a Diff object with the content specified.
32 sio = StringIO()
33 sio.write(content)
34 size = sio.tell()
35 sio.seek(0)
36 diff = Diff.fromFile(sio, size)
37 # Commit to make the alias available for reading.
38 transaction.commit()
39 return diff
40
41 def test_text_reads_librarian_content(self):
42 # IDiff.text will read at most config.diff.max_read_size bytes from
43 # the librarian.
44 content = "1234567890" * 10
45 diff = self._create_diff(content)
46 self.assertEqual(content, diff.text)
47
48 def test_oversized_normal(self):
49 # A diff smaller than config.diff.max_read_size is not oversized.
50 content = "1234567890" * 10
51 diff = self._create_diff(content)
52 self.assertFalse(diff.oversized)
53
54 def test_text_read_limited_by_config(self):
55 # IDiff.text will read at most config.diff.max_read_size bytes from
56 # the librarian.
57 self.pushConfig("diff", max_read_size=25)
58 content = "1234567890" * 10
59 diff = self._create_diff(content)
60 self.assertEqual(content[:25], diff.text)
61
62 def test_oversized_for_big_diff(self):
63 # A diff larger than config.diff.max_read_size is oversized.
64 self.pushConfig("diff", max_read_size=25)
65 content = "1234567890" * 10
66 diff = self._create_diff(content)
67 self.assertTrue(diff.oversized)
68
3069
31class TestStaticDiff(TestCaseWithFactory):70class TestStaticDiff(TestCaseWithFactory):
32 """Test that StaticDiff objects work."""71 """Test that StaticDiff objects work."""
3372
=== modified file 'lib/lp/code/model/tests/test_revision.py'
--- lib/lp/code/model/tests/test_revision.py 2009-06-25 04:06:00 +0000
+++ lib/lp/code/model/tests/test_revision.py 2009-08-02 23:17:10 +0000
@@ -146,7 +146,8 @@
146 branch.createBranchRevision(1, rev)146 branch.createBranchRevision(1, rev)
147 # Once the branch is connected to the revision, we now specify147 # Once the branch is connected to the revision, we now specify
148 # a product for the branch.148 # a product for the branch.
149 branch.product = self.factory.makeProduct()149 project = self.factory.makeProduct()
150 branch.setTarget(user=branch.owner, project=project)
150 # The revision is now identified as needing karma allocated.151 # The revision is now identified as needing karma allocated.
151 self.assertEqual(152 self.assertEqual(
152 [rev], list(RevisionSet.getRevisionsNeedingKarmaAllocated()))153 [rev], list(RevisionSet.getRevisionsNeedingKarmaAllocated()))
153154
=== modified file 'lib/lp/code/scripts/tests/test_revisionkarma.py'
--- lib/lp/code/scripts/tests/test_revisionkarma.py 2009-06-25 04:06:00 +0000
+++ lib/lp/code/scripts/tests/test_revisionkarma.py 2009-08-04 00:41:49 +0000
@@ -49,7 +49,8 @@
49 branch.createBranchRevision(1, rev)49 branch.createBranchRevision(1, rev)
50 # Once the branch is connected to the revision, we now specify50 # Once the branch is connected to the revision, we now specify
51 # a product for the branch.51 # a product for the branch.
52 branch.product = self.factory.makeProduct()52 project = self.factory.makeProduct()
53 branch.setTarget(user=branch.owner, project=project)
53 # Commit and switch to the script db user.54 # Commit and switch to the script db user.
54 transaction.commit()55 transaction.commit()
55 LaunchpadZopelessLayer.switchDbUser(config.revisionkarma.dbuser)56 LaunchpadZopelessLayer.switchDbUser(config.revisionkarma.dbuser)
5657
=== modified file 'lib/lp/code/templates/branchmergeproposal-index.pt'
--- lib/lp/code/templates/branchmergeproposal-index.pt 2009-08-04 05:02:41 +0000
+++ lib/lp/code/templates/branchmergeproposal-index.pt 2009-08-05 02:18:13 +0000
@@ -139,6 +139,10 @@
139 <div class="boardCommentBody attachmentBody">139 <div class="boardCommentBody attachmentBody">
140 <tal:diff replace="structure view/review_diff/fmt:diff" />140 <tal:diff replace="structure view/review_diff/fmt:diff" />
141 </div>141 </div>
142 <div class="boardCommentFooter"
143 tal:condition="view/review_diff_oversized">
144 The diff has been truncated for viewing.
145 </div>
142 </tal:real-diff>146 </tal:real-diff>
143 <tal:empty-diff condition="not: attachment">147 <tal:empty-diff condition="not: attachment">
144 <div class="boardCommentBody attachmentBody">148 <div class="boardCommentBody attachmentBody">
145149
=== modified file 'lib/lp/codehosting/tests/test_acceptance.py'
--- lib/lp/codehosting/tests/test_acceptance.py 2009-07-23 18:10:26 +0000
+++ lib/lp/codehosting/tests/test_acceptance.py 2009-08-04 00:41:49 +0000
@@ -382,7 +382,7 @@
382 # rename as far as bzr is concerned: the URL changes.382 # rename as far as bzr is concerned: the URL changes.
383 LaunchpadZopelessTestSetup().txn.begin()383 LaunchpadZopelessTestSetup().txn.begin()
384 branch = self.getDatabaseBranch('testuser', None, 'test-branch')384 branch = self.getDatabaseBranch('testuser', None, 'test-branch')
385 branch.product = Product.byName('firefox')385 branch.setTarget(user=branch.owner, project=Product.byName('firefox'))
386 LaunchpadZopelessTestSetup().txn.commit()386 LaunchpadZopelessTestSetup().txn.commit()
387387
388 self.assertNotBranch(388 self.assertNotBranch(
389389
=== modified file 'lib/lp/registry/browser/tests/private-team-creation-views.txt'
--- lib/lp/registry/browser/tests/private-team-creation-views.txt 2009-07-31 02:45:01 +0000
+++ lib/lp/registry/browser/tests/private-team-creation-views.txt 2009-08-03 21:46:09 +0000
@@ -400,8 +400,7 @@
400 ... bugtask = bug.default_bugtask400 ... bugtask = bug.default_bugtask
401 ... bugtask.transitionToAssignee(team)401 ... bugtask.transitionToAssignee(team)
402 ... # A branch.402 ... # A branch.
403 ... branch = factory.makeBranch()403 ... branch = factory.makeBranch(owner=team, registrant=team_owner)
404 ... branch.owner = team
405 ... # A branch subscription.404 ... # A branch subscription.
406 ... from lp.code.enums import (405 ... from lp.code.enums import (
407 ... BranchSubscriptionDiffSize,406 ... BranchSubscriptionDiffSize,
408407
=== modified file 'lib/lp/registry/doc/private-team-roles.txt'
--- lib/lp/registry/doc/private-team-roles.txt 2009-07-31 02:45:01 +0000
+++ lib/lp/registry/doc/private-team-roles.txt 2009-08-03 01:41:07 +0000
@@ -23,9 +23,10 @@
23-----------------23-----------------
2424
25 >>> # Create the necessary teams.25 >>> # Create the necessary teams.
26 >>> from lp.registry.interfaces.person import PersonVisibility
27 >>> team_owner = factory.makePerson(name='team-owner')26 >>> team_owner = factory.makePerson(name='team-owner')
28 >>> login('foo.bar@canonical.com')27 >>> from lp.registry.interfaces.person import IPersonSet, PersonVisibility
28 >>> admin_user = getUtility(IPersonSet).getByEmail('admin@canonical.com')
29 >>> login_person(admin_user)
29 >>> priv_team = factory.makeTeam(name='private-team',30 >>> priv_team = factory.makeTeam(name='private-team',
30 ... owner=team_owner,31 ... owner=team_owner,
31 ... visibility=PersonVisibility.PRIVATE)32 ... visibility=PersonVisibility.PRIVATE)
@@ -102,12 +103,12 @@
102Private teams can be assigned as the owner of a branch103Private teams can be assigned as the owner of a branch
103104
104 >>> branch = factory.makeBranch()105 >>> branch = factory.makeBranch()
105 >>> branch.owner = priv_team106 >>> branch.setOwner(priv_team, user=admin_user)
106107
107But private membership teams cannot own a branch.108But private membership teams cannot own a branch.
108109
109 >>> branch = factory.makeBranch()110 >>> branch = factory.makeBranch()
110 >>> branch.owner = pm_team111 >>> branch.setOwner(pm_team, user=admin_user)
111 Traceback (most recent call last):112 Traceback (most recent call last):
112 ...113 ...
113 PrivatePersonLinkageError: Cannot link person114 PrivatePersonLinkageError: Cannot link person
114115
=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py 2009-07-23 17:47:50 +0000
+++ lib/lp/testing/factory.py 2009-08-03 21:46:09 +0000
@@ -600,7 +600,10 @@
600 distroseries = sourcepackage.distroseries600 distroseries = sourcepackage.distroseries
601601
602 if registrant is None:602 if registrant is None:
603 registrant = owner603 if owner.is_team:
604 registrant = owner.teamowner
605 else:
606 registrant = owner
604607
605 if branch_type in (BranchType.HOSTED, BranchType.IMPORTED):608 if branch_type in (BranchType.HOSTED, BranchType.IMPORTED):
606 url = None609 url = None
607610
=== modified file 'lib/lp/translations/scripts/message_sharing_migration.py'
--- lib/lp/translations/scripts/message_sharing_migration.py 2009-07-19 04:41:14 +0000
+++ lib/lp/translations/scripts/message_sharing_migration.py 2009-08-05 10:45:31 +0000
@@ -309,8 +309,8 @@
309 message = removeSecurityProxy(message)309 message = removeSecurityProxy(message)
310310
311 clashing_current, clashing_imported, twin = (311 clashing_current, clashing_imported, twin = (
312 self._findClashesFromDicts(312 self._findClashes(
313 existing_tms, current_tms, imported_tms, message))313 message, representative, message.potemplate))
314314
315 if clashing_current or clashing_imported:315 if clashing_current or clashing_imported:
316 saved = self._saveByDiverging(316 saved = self._saveByDiverging(

Subscribers

People subscribed via source and target branches

to status/vote changes: