Merge lp:~jtv/launchpad/cp-bug-403992 into lp:launchpad/db-devel
- cp-bug-403992
- Merge into 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 | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Canonical Launchpad Engineering | Pending | ||
Review via email: mp+9689@code.launchpad.net |
Commit message
Description of the change
To post a comment you must log in.
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote : | # |
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'lib/canonical/config/schema-lazr.conf' |
2 | --- lib/canonical/config/schema-lazr.conf 2009-07-29 09:46:46 +0000 |
3 | +++ lib/canonical/config/schema-lazr.conf 2009-08-05 02:18:13 +0000 |
4 | @@ -584,6 +584,16 @@ |
5 | # in refcounts and memory. |
6 | references_leak_log: /tmp/references-leak.log |
7 | |
8 | +[diff] |
9 | +# The maximum size in bytes to read from the librarian to make available in |
10 | +# the web UI. 512k == 524288 bytes. |
11 | +# datatype: integer |
12 | +max_read_size: 524288 |
13 | + |
14 | +# The maximum number of lines to format using the format_diff tal formatter. |
15 | +max_format_lines: 5000 |
16 | + |
17 | + |
18 | [distributionmirrorprober] |
19 | # The database user which will be used by this process. |
20 | # datatype: string |
21 | |
22 | === modified file 'lib/canonical/launchpad/emailtemplates/branch-merge-proposal-created.txt' |
23 | --- lib/canonical/launchpad/emailtemplates/branch-merge-proposal-created.txt 2008-11-26 07:40:34 +0000 |
24 | +++ lib/canonical/launchpad/emailtemplates/branch-merge-proposal-created.txt 2009-08-04 09:15:32 +0000 |
25 | @@ -1,6 +1,6 @@ |
26 | %(proposal_registrant)s has proposed merging %(source_branch)s into %(target_branch)s. |
27 | |
28 | -%(reviews)s%(gap)s%(comment)s |
29 | +%(reviews)s%(gap)s%(comment)s%(diff_cutoff_warning)s |
30 | -- |
31 | %(proposal_url)s |
32 | %(reason)s%(edit_subscription)s |
33 | |
34 | === modified file 'lib/canonical/launchpad/emailtemplates/review-requested.txt' |
35 | --- lib/canonical/launchpad/emailtemplates/review-requested.txt 2009-02-15 23:44:45 +0000 |
36 | +++ lib/canonical/launchpad/emailtemplates/review-requested.txt 2009-08-04 09:15:32 +0000 |
37 | @@ -1,7 +1,7 @@ |
38 | You have been requested to review the proposed merge of %(source_branch)s into %(target_branch)s. |
39 | |
40 | -%(comment)s |
41 | +%(comment)s%(diff_cutoff_warning)s |
42 | |
43 | --- |
44 | +-- |
45 | %(proposal_url)s |
46 | %(reason)s%(edit_subscription)s |
47 | |
48 | === modified file 'lib/canonical/launchpad/interfaces/_schema_circular_imports.py' |
49 | --- lib/canonical/launchpad/interfaces/_schema_circular_imports.py 2009-08-04 05:02:41 +0000 |
50 | +++ lib/canonical/launchpad/interfaces/_schema_circular_imports.py 2009-08-05 07:35:57 +0000 |
51 | @@ -75,6 +75,11 @@ |
52 | IBranch['linkSpecification'].queryTaggedValue( |
53 | LAZR_WEBSERVICE_EXPORTED)['params']['spec'].schema= ISpecification |
54 | IBranch['product'].schema = IProduct |
55 | +IBranch['setTarget'].queryTaggedValue( |
56 | + LAZR_WEBSERVICE_EXPORTED)['params']['project'].schema= IProduct |
57 | +IBranch['setTarget'].queryTaggedValue( |
58 | + LAZR_WEBSERVICE_EXPORTED)['params']['source_package'].schema= \ |
59 | + ISourcePackage |
60 | IBranch['spec_links'].value_type.schema = ISpecificationBranch |
61 | IBranch['subscribe'].queryTaggedValue( |
62 | LAZR_WEBSERVICE_EXPORTED)['return_type'].schema = IBranchSubscription |
63 | |
64 | === modified file 'lib/canonical/launchpad/javascript/bugs/bugtask-index.js' |
65 | --- lib/canonical/launchpad/javascript/bugs/bugtask-index.js 2009-08-04 12:26:40 +0000 |
66 | +++ lib/canonical/launchpad/javascript/bugs/bugtask-index.js 2009-08-05 09:33:08 +0000 |
67 | @@ -1319,6 +1319,155 @@ |
68 | } |
69 | }; |
70 | |
71 | +/** |
72 | + * Set up the "me too" selection. |
73 | + * |
74 | + * Called once, on load, to initialize the page. Call this function if |
75 | + * the "me too" information is displayed on a bug page and the user is |
76 | + * logged in. |
77 | + * |
78 | + * @method setup_me_too |
79 | + */ |
80 | +bugs.setup_me_too = function(user_is_affected) { |
81 | + var me_too_content = Y.get('#affectsmetoo'); |
82 | + var me_too_edit = new MeTooChoiceSource({ |
83 | + contentBox: me_too_content, value: user_is_affected, |
84 | + elementToFlash: me_too_content |
85 | + }); |
86 | + me_too_edit.render(); |
87 | +}; |
88 | + |
89 | +/** |
90 | + * This class is a derivative of ChoiceSource that handles the |
91 | + * specifics of editing "me too" option. |
92 | + * |
93 | + * @class MeTooChoiceSource |
94 | + * @extends ChoiceSource |
95 | + * @constructor |
96 | + */ |
97 | +function MeTooChoiceSource() { |
98 | + MeTooChoiceSource.superclass.constructor.apply(this, arguments); |
99 | +} |
100 | + |
101 | +MeTooChoiceSource.NAME = 'metoocs'; |
102 | +MeTooChoiceSource.NS = 'metoocs'; |
103 | + |
104 | +MeTooChoiceSource.HTML_PARSER = { |
105 | + flame_icon: ".dynamic img[src$=/@@/flame-icon]" |
106 | +}; |
107 | + |
108 | +MeTooChoiceSource.ATTRS = { |
109 | + /** |
110 | + * The title is always the same, so bake it in here. |
111 | + * |
112 | + * @attribute title |
113 | + * @type String |
114 | + */ |
115 | + title: { |
116 | + value: 'Does this bug affect you?' |
117 | + }, |
118 | + |
119 | + /** |
120 | + * The items are always the same, so bake them in here. |
121 | + * |
122 | + * @attribute items |
123 | + * @type Array |
124 | + */ |
125 | + items: { |
126 | + value: [ |
127 | + { name: 'Yes, it affects me', value: true, |
128 | + source_name: 'This bug affects me too', |
129 | + disabled: false }, |
130 | + { name: "No, it doesn't affect me", value: false, |
131 | + source_name: "This bug doesn't affect me", |
132 | + disabled: false } |
133 | + ] |
134 | + }, |
135 | + |
136 | + /** |
137 | + * Y.Node containing a flame icon, displayed when the user is |
138 | + * affected by the current bug. Should be automatically calculated |
139 | + * by HTML_PARSER. |
140 | + * |
141 | + * Setter function returns Y.get(parameter) so that you can pass |
142 | + * either a Node (as expected) or a selector. |
143 | + * |
144 | + * @attribute value_location |
145 | + * @type Node |
146 | + */ |
147 | + flame_icon: { |
148 | + value: null, |
149 | + set: function(v) { |
150 | + return Y.get(v); |
151 | + } |
152 | + } |
153 | +}; |
154 | + |
155 | +// Put this in the bugs namespace so it can be accessed for testing. |
156 | +bugs._MeTooChoiceSource = MeTooChoiceSource; |
157 | + |
158 | +Y.extend(MeTooChoiceSource, Y.ChoiceSource, { |
159 | + initializer: function() { |
160 | + var widget = this; |
161 | + this.error_handler = new LP.client.ErrorHandler(); |
162 | + this.error_handler.clearProgressUI = function() { |
163 | + widget._uiClearWaiting(); |
164 | + }; |
165 | + this.error_handler.showError = function(error_msg) { |
166 | + widget.showError(error_msg); |
167 | + }; |
168 | + }, |
169 | + |
170 | + showError: function(err) { |
171 | + display_error(null, err); |
172 | + }, |
173 | + |
174 | + syncUI: function() { |
175 | + MeTooChoiceSource.superclass.syncUI.apply(this, arguments); |
176 | + // Show the flame icon if the user is affected by this bug. |
177 | + if (this.get('value')) { |
178 | + this.get('flame_icon').removeClass('unseen'); |
179 | + } else { |
180 | + this.get('flame_icon').addClass('unseen'); |
181 | + } |
182 | + }, |
183 | + |
184 | + render: function() { |
185 | + MeTooChoiceSource.superclass.render.apply(this, arguments); |
186 | + // Force the ChoiceSource to be rendered inline. |
187 | + this.get('boundingBox').setStyle('display', 'inline'); |
188 | + // Hide the static content and show the dynamic content. |
189 | + this.get('contentBox').query('.static').addClass('unseen'); |
190 | + this.get('contentBox').query('.dynamic').removeClass('unseen'); |
191 | + }, |
192 | + |
193 | + _saveData: function() { |
194 | + // Set the widget to the 'waiting' state. |
195 | + this._uiSetWaiting(); |
196 | + |
197 | + var value = this.getInput(); |
198 | + var client = new LP.client.Launchpad(); |
199 | + var widget = this; |
200 | + |
201 | + var config = { |
202 | + on: { |
203 | + success: function(entry) { |
204 | + widget._uiClearWaiting(); |
205 | + MeTooChoiceSource.superclass._saveData.call( |
206 | + widget, value); |
207 | + }, |
208 | + failure: this.error_handler.getFailureHandler() |
209 | + }, |
210 | + parameters: { |
211 | + affected: value |
212 | + } |
213 | + }; |
214 | + |
215 | + client.named_post( |
216 | + LP.client.cache.bug.self_link, 'markUserAffected', config); |
217 | + } |
218 | +}); |
219 | + |
220 | /* |
221 | * Check if the current user can unsubscribe the person |
222 | * being subscribed. |
223 | @@ -1503,6 +1652,6 @@ |
224 | } |
225 | |
226 | }, '0.1', {requires: ['base', 'oop', 'node', 'event', 'io-base', 'substitute', |
227 | - 'widget-position-ext', 'lazr.formoverlay', 'lazr.anim', |
228 | + 'widget-position-ext', 'lazr.formoverlay', 'lazr.anim', |
229 | 'lazr.base', 'lazr.overlay', 'lazr.choiceedit', |
230 | 'lp.picker', 'lp.client.plugins', 'lp.subscriber']}); |
231 | |
232 | === added file 'lib/canonical/launchpad/javascript/bugs/tests/test_me_too.html' |
233 | --- lib/canonical/launchpad/javascript/bugs/tests/test_me_too.html 1970-01-01 00:00:00 +0000 |
234 | +++ lib/canonical/launchpad/javascript/bugs/tests/test_me_too.html 2009-08-04 16:48:41 +0000 |
235 | @@ -0,0 +1,34 @@ |
236 | +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> |
237 | +<html> |
238 | + <head> |
239 | + <title>Status Editor</title> |
240 | + |
241 | + <!-- YUI 3.0 Setup --> |
242 | + <script type="text/javascript" src="../../../icing/yui/3.0.0pr2/build/yui/yui.js"></script> |
243 | + <link rel="stylesheet" href="../../../icing/yui/3.0.0pr2/build/cssreset/reset.css"/> |
244 | + <link rel="stylesheet" href="../../../icing/yui/3.0.0pr2/build/cssfonts/fonts.css"/> |
245 | + <link rel="stylesheet" href="../../../icing/yui/3.0.0pr2/build/cssbase/base.css"/> |
246 | + |
247 | + <!-- Dependency --> |
248 | + <script type="text/javascript" src="../../../icing/lazr/build/lazr.js"></script> |
249 | + <script type="text/javascript" src="../../../icing/lazr/build/overlay/overlay.js"></script> |
250 | + <script type="text/javascript" src="../../../icing/lazr/build/choiceedit/choiceedit.js"></script> |
251 | + <script type="text/javascript" src="../../../javascript/client/client.js"></script> |
252 | + |
253 | + <!-- The module under test --> |
254 | + <script type="text/javascript" src="../bugtask-index.js"></script> |
255 | + |
256 | + <!-- The test suite --> |
257 | + <script type="text/javascript" src="test_me_too.js"></script> |
258 | + |
259 | + <!-- Test layout --> |
260 | + <link rel="stylesheet" href="../../test.css" /> |
261 | + <style type="text/css"> |
262 | + /* CSS classes specific to this test */ |
263 | + .unseen { display: none; } |
264 | + </style> |
265 | +</head> |
266 | +<body class="yui-skin-sam"> |
267 | + <div id="log"></div> |
268 | +</body> |
269 | +</html> |
270 | |
271 | === added file 'lib/canonical/launchpad/javascript/bugs/tests/test_me_too.js' |
272 | --- lib/canonical/launchpad/javascript/bugs/tests/test_me_too.js 1970-01-01 00:00:00 +0000 |
273 | +++ lib/canonical/launchpad/javascript/bugs/tests/test_me_too.js 2009-08-04 16:19:27 +0000 |
274 | @@ -0,0 +1,223 @@ |
275 | +/* Copyright (c) 2008, Canonical Ltd. All rights reserved. */ |
276 | + |
277 | +YUI({ |
278 | + base: '../../../icing/yui/current/build/', |
279 | + filter: 'raw', |
280 | + combine: false |
281 | + }).use('event', 'bugs.bugtask_index', 'node', 'yuitest', 'widget-stack', 'console', |
282 | + function(Y) { |
283 | + |
284 | +// Local aliases |
285 | +var Assert = Y.Assert, |
286 | + ArrayAssert = Y.ArrayAssert; |
287 | + |
288 | +/* |
289 | + * A wrapper for the Y.Event.simulate() function. The wrapper accepts |
290 | + * CSS selectors and Node instances instead of raw nodes. |
291 | + */ |
292 | +function simulate(widget, selector, evtype, options) { |
293 | + var rawnode = Y.Node.getDOMNode(widget.query(selector)); |
294 | + Y.Event.simulate(rawnode, evtype, options); |
295 | +} |
296 | + |
297 | +/* Helper function to clean up a dynamically added widget instance. */ |
298 | +function cleanup_widget(widget) { |
299 | + // Nuke the boundingBox, but only if we've touched the DOM. |
300 | + if (widget.get('rendered')) { |
301 | + var bb = widget.get('boundingBox'); |
302 | + if (bb.get('parentNode')) { |
303 | + bb.get('parentNode').removeChild(bb); |
304 | + } |
305 | + } |
306 | + // Kill the widget itself. |
307 | + widget.destroy(); |
308 | +} |
309 | + |
310 | +var suite = new Y.Test.Suite("Bugtask Me-Too Choice Edit Tests"); |
311 | + |
312 | +suite.add(new Y.Test.Case({ |
313 | + |
314 | + name: 'me_too_choice_edit_basics', |
315 | + |
316 | + setUp: function() { |
317 | + // Monkeypatch LP.client to avoid network traffic and to make |
318 | + // some things work as expected. |
319 | + LP.client.Launchpad.prototype.named_post = |
320 | + function(url, func, config) { |
321 | + config.on.success(); |
322 | + }; |
323 | + LP.client.cache.bug = { |
324 | + self_link: "http://bugs.example.com/bugs/1234" |
325 | + }; |
326 | + // add the in-page HTML |
327 | + var inpage = Y.Node.create([ |
328 | + '<span id="affectsmetoo">', |
329 | + ' <span class="static">', |
330 | + ' <img src="https://bugs.edge.launchpad.net/@@/flame-icon" alt="" />', |
331 | + ' This bug affects me too', |
332 | + ' <a href="+affectsmetoo">', |
333 | + ' <img class="editicon" alt="Edit"', |
334 | + ' src="https://bugs.edge.launchpad.net/@@/edit" />', |
335 | + ' </a>', |
336 | + ' </span>', |
337 | + ' <span class="dynamic unseen">', |
338 | + ' <img class="editicon" alt="Edit"', |
339 | + ' src="https://bugs.edge.launchpad.net/@@/edit" />', |
340 | + ' <a href="+affectsmetoo" class="js-action"', |
341 | + ' ><span class="value">Does this bug affect you?</span></a>', |
342 | + ' <img src="https://bugs.edge.launchpad.net/@@/flame-icon" alt=""/>', |
343 | + ' </span>', |
344 | + '</span>'].join('')); |
345 | + Y.get("body").appendChild(inpage); |
346 | + var me_too_content = Y.get('#affectsmetoo'); |
347 | + this.config = { |
348 | + contentBox: me_too_content, value: null, |
349 | + elementToFlash: me_too_content |
350 | + }; |
351 | + this.choice_edit = new Y.bugs._MeTooChoiceSource(this.config); |
352 | + this.choice_edit.render(); |
353 | + }, |
354 | + |
355 | + tearDown: function() { |
356 | + if (this.choice_edit._choice_list) { |
357 | + cleanup_widget(this.choice_edit._choice_list); |
358 | + } |
359 | + var status = Y.get("document").query("#affectsmetoo"); |
360 | + if (status) { |
361 | + status.get("parentNode").removeChild(status); |
362 | + } |
363 | + }, |
364 | + |
365 | + /** |
366 | + * The choice edit should be displayed inline. |
367 | + */ |
368 | + test_is_inline: function() { |
369 | + var display = this.choice_edit.get('boundingBox').getStyle('display'); |
370 | + Assert.areEqual( |
371 | + display, 'inline', "Not displayed inline, display is: " + display); |
372 | + }, |
373 | + |
374 | + /** |
375 | + * The .static area should be hidden by adding the "unseen" class. |
376 | + */ |
377 | + test_hide_static: function() { |
378 | + var static_area = this.choice_edit.get('contentBox').query('.static'); |
379 | + Assert.isTrue( |
380 | + static_area.hasClass('unseen'), "Static area is not hidden."); |
381 | + }, |
382 | + |
383 | + /** |
384 | + * The .dynamic area should be shown by removing the "unseen" class. |
385 | + */ |
386 | + test_hide_dynamic: function() { |
387 | + var dynamic_area = this.choice_edit.get('contentBox').query('.dynamic'); |
388 | + Assert.isFalse( |
389 | + dynamic_area.hasClass('unseen'), "Dynamic area is hidden."); |
390 | + }, |
391 | + |
392 | + /** |
393 | + * The flame icon should be hidden initially. |
394 | + */ |
395 | + test_flame_hidden_initially: function() { |
396 | + var flame_icon = this.choice_edit.get('flame_icon'); |
397 | + Assert.isTrue(flame_icon.hasClass('unseen'), "Flame is not hidden."); |
398 | + }, |
399 | + |
400 | + /** |
401 | + * The flame icon should be hidden when the user has made a |
402 | + * negative choice (i.e. "Does not affect me"). |
403 | + */ |
404 | + test_flame_hidden_with_negative_choice: function() { |
405 | + simulate(this.choice_edit.get('boundingBox'), '.value', 'mousedown'); |
406 | + simulate(this.choice_edit._choice_list.get('boundingBox'), |
407 | + 'li a[href$=false]', 'click'); |
408 | + var flame_icon = this.choice_edit.get('flame_icon'); |
409 | + Assert.isTrue(flame_icon.hasClass('unseen'), "Flame is not hidden."); |
410 | + }, |
411 | + |
412 | + /** |
413 | + * The flame icon should be shown when the user has made a |
414 | + * positive choice (i.e. "Affects me too"). |
415 | + */ |
416 | + test_flame_hidden_with_positive_choice: function() { |
417 | + simulate(this.choice_edit.get('boundingBox'), '.value', 'mousedown'); |
418 | + simulate(this.choice_edit._choice_list.get('boundingBox'), |
419 | + 'li a[href$=true]', 'click'); |
420 | + var flame_icon = this.choice_edit.get('flame_icon'); |
421 | + Assert.isFalse(flame_icon.hasClass('unseen'), "Flame is hidden."); |
422 | + }, |
423 | + |
424 | + /** |
425 | + * The UI should be in a waiting state while the save process is |
426 | + * executing and return to a non-waiting state once it has |
427 | + * finished. |
428 | + */ |
429 | + test_ui_waiting_for_success: function() { |
430 | + this.do_test_ui_waiting('success'); |
431 | + }, |
432 | + |
433 | + /** |
434 | + * The UI should be in a waiting state while the save process is |
435 | + * executing and return to a non-waiting state even if the process |
436 | + * fails. |
437 | + */ |
438 | + test_ui_waiting_for_failure: function() { |
439 | + this.do_test_ui_waiting('failure'); |
440 | + }, |
441 | + |
442 | + /** |
443 | + * Helper function that does the leg work for the |
444 | + * test_ui_waiting_* methods. |
445 | + */ |
446 | + do_test_ui_waiting: function(callback) { |
447 | + var edit_icon = this.choice_edit.get('editicon'); |
448 | + // The spinner should not be displayed at first. |
449 | + Assert.isNull( |
450 | + edit_icon.get('src').match(/\/spinner$/), |
451 | + "The edit icon is displaying a spinner at rest."); |
452 | + // The spinner should not be displayed after opening the |
453 | + // choice list. |
454 | + simulate(this.choice_edit.get('boundingBox'), '.value', 'mousedown'); |
455 | + Assert.isNull( |
456 | + edit_icon.get('src').match(/\/spinner$/), |
457 | + "The edit icon is displaying a spinner after opening the choice list."); |
458 | + // The spinner should be visible during the interval between a |
459 | + // choice being made and a response coming back from Launchpad |
460 | + // that the choice has been saved. |
461 | + var edit_icon_src_during_save; |
462 | + // Patch the named_post method to simulate success or failure, |
463 | + // as determined by the callback argument. We cannot make |
464 | + // assertions in this method because exceptions are swallowed |
465 | + // somewhere. Instead, we save something testable to a local |
466 | + // var. |
467 | + LP.client.Launchpad.prototype.named_post = |
468 | + function(url, func, config) { |
469 | + edit_icon_src_during_save = edit_icon.get('src'); |
470 | + config.on[callback](); |
471 | + }; |
472 | + simulate(this.choice_edit._choice_list.get('boundingBox'), |
473 | + 'li a[href$=true]', 'click'); |
474 | + Assert.isNotNull( |
475 | + edit_icon_src_during_save.match(/\/spinner$/), |
476 | + "The edit icon is not displaying a spinner during save."); |
477 | + // The spinner should not be displayed once a choice has been |
478 | + // saved. |
479 | + Assert.isNull( |
480 | + edit_icon.get('src').match(/\/spinner$/), |
481 | + "The edit icon is displaying a spinner once the choice has been made."); |
482 | + } |
483 | + |
484 | +})); |
485 | + |
486 | +Y.Test.Runner.add(suite); |
487 | + |
488 | +var yconsole = new Y.Console({ |
489 | + newestOnTop: false |
490 | +}); |
491 | +yconsole.render('#log'); |
492 | + |
493 | +Y.on('domready', function() { |
494 | + Y.Test.Runner.run(); |
495 | +}); |
496 | + |
497 | +}); |
498 | |
499 | === modified file 'lib/canonical/launchpad/webapp/adapter.py' |
500 | --- lib/canonical/launchpad/webapp/adapter.py 2009-07-28 22:35:01 +0000 |
501 | +++ lib/canonical/launchpad/webapp/adapter.py 2009-07-29 11:37:51 +0000 |
502 | @@ -134,7 +134,7 @@ |
503 | def get_request_statements(): |
504 | """Get the list of executed statements in the request. |
505 | |
506 | - The list is composed of (starttime, endtime, statement) tuples. |
507 | + The list is composed of (starttime, endtime, db_id, statement) tuples. |
508 | Times are given in milliseconds since the start of the request. |
509 | """ |
510 | return getattr(_local, 'request_statements', []) |
511 | @@ -165,7 +165,11 @@ |
512 | # convert times to integer millisecond values |
513 | starttime = int((starttime - request_starttime) * 1000) |
514 | endtime = int((endtime - request_starttime) * 1000) |
515 | - _local.request_statements.append((starttime, endtime, statement)) |
516 | + # A string containing no whitespace that lets us identify which Store |
517 | + # is being used. |
518 | + database_identifier = connection_wrapper._database.name |
519 | + _local.request_statements.append( |
520 | + (starttime, endtime, database_identifier, statement)) |
521 | |
522 | # store the last executed statement as an attribute on the current |
523 | # thread |
524 | @@ -274,6 +278,8 @@ |
525 | # opinion on what uri is. |
526 | # pylint: disable-msg=W0231 |
527 | self._uri = uri |
528 | + # A unique name for this database connection. |
529 | + self.name = uri.database |
530 | |
531 | def raw_connect(self): |
532 | # Prevent database connections from the main thread if |
533 | @@ -348,6 +354,9 @@ |
534 | |
535 | class LaunchpadSessionDatabase(Postgres): |
536 | |
537 | + # A unique name for this database connection. |
538 | + name = 'session' |
539 | + |
540 | def raw_connect(self): |
541 | self._dsn = 'dbname=%s user=%s' % (config.launchpad_session.dbname, |
542 | config.launchpad_session.dbuser) |
543 | |
544 | === modified file 'lib/canonical/launchpad/webapp/errorlog.py' |
545 | --- lib/canonical/launchpad/webapp/errorlog.py 2009-07-23 05:02:47 +0000 |
546 | +++ lib/canonical/launchpad/webapp/errorlog.py 2009-08-05 07:34:55 +0000 |
547 | @@ -167,9 +167,9 @@ |
548 | fp.write('%s=%s\n' % (urllib.quote(key, safe_chars), |
549 | urllib.quote(value, safe_chars))) |
550 | fp.write('\n') |
551 | - for (start, end, statement) in self.db_statements: |
552 | - fp.write('%05d-%05d %s\n' % (start, end, |
553 | - _normalise_whitespace(statement))) |
554 | + for (start, end, database_id, statement) in self.db_statements: |
555 | + fp.write('%05d-%05d@%s %s\n' % ( |
556 | + start, end, database_id, _normalise_whitespace(statement))) |
557 | fp.write('\n') |
558 | fp.write(self.tb_text) |
559 | |
560 | @@ -206,9 +206,12 @@ |
561 | line = line.strip() |
562 | if line == '': |
563 | break |
564 | - startend, statement = line.split(' ', 1) |
565 | - start, end = startend.split('-') |
566 | - statements.append((int(start), int(end), statement)) |
567 | + start, end, db_id, statement = re.match( |
568 | + r'^(\d+)-(\d+)(?:@([\w-]+))?\s+(.*)', line).groups() |
569 | + if db_id is not None: |
570 | + db_id = intern(db_id) # This string is repeated lots. |
571 | + statements.append( |
572 | + (int(start), int(end), db_id, statement)) |
573 | |
574 | # The rest is traceback. |
575 | tb_text = ''.join(lines) |
576 | @@ -471,9 +474,10 @@ |
577 | |
578 | duration = get_request_duration() |
579 | |
580 | - statements = sorted((start, end, _safestr(statement)) |
581 | - for (start, end, statement) |
582 | - in get_request_statements()) |
583 | + statements = sorted( |
584 | + (start, end, _safestr(database_id), _safestr(statement)) |
585 | + for (start, end, database_id, statement) |
586 | + in get_request_statements()) |
587 | |
588 | oopsid, filename = self.newOopsId(now) |
589 | |
590 | |
591 | === modified file 'lib/canonical/launchpad/webapp/ftests/test_adapter.txt' |
592 | --- lib/canonical/launchpad/webapp/ftests/test_adapter.txt 2009-04-17 10:32:16 +0000 |
593 | +++ lib/canonical/launchpad/webapp/ftests/test_adapter.txt 2009-08-04 12:14:57 +0000 |
594 | @@ -292,7 +292,7 @@ |
595 | >>> set_request_started() |
596 | >>> store.execute('SELECT 1', noresult=True) |
597 | >>> store.execute('SELECT 2', noresult=True) |
598 | - >>> for starttime, endtime, statement in get_request_statements(): |
599 | + >>> for starttime, endtime, db_id, statement in get_request_statements(): |
600 | ... print statement |
601 | SELECT 1 |
602 | SELECT 2 |
603 | |
604 | === modified file 'lib/canonical/launchpad/webapp/launchpadform.py' |
605 | --- lib/canonical/launchpad/webapp/launchpadform.py 2009-06-25 05:30:52 +0000 |
606 | +++ lib/canonical/launchpad/webapp/launchpadform.py 2009-08-04 00:41:49 +0000 |
607 | @@ -372,7 +372,7 @@ |
608 | |
609 | render_context = True |
610 | |
611 | - def updateContextFromData(self, data, context=None): |
612 | + def updateContextFromData(self, data, context=None, notify_modified=True): |
613 | """Update the context object based on form data. |
614 | |
615 | If no context is given, the view's context is used. |
616 | @@ -386,12 +386,13 @@ |
617 | """ |
618 | if context is None: |
619 | context = self.context |
620 | - context_before_modification = Snapshot( |
621 | - context, providing=providedBy(context)) |
622 | + if notify_modified: |
623 | + context_before_modification = Snapshot( |
624 | + context, providing=providedBy(context)) |
625 | |
626 | was_changed = form.applyChanges(context, self.form_fields, |
627 | data, self.adapters) |
628 | - if was_changed: |
629 | + if was_changed and notify_modified: |
630 | field_names = [form_field.__name__ |
631 | for form_field in self.form_fields] |
632 | notify(ObjectModifiedEvent( |
633 | |
634 | === modified file 'lib/canonical/launchpad/webapp/tales.py' |
635 | --- lib/canonical/launchpad/webapp/tales.py 2009-08-03 15:21:35 +0000 |
636 | +++ lib/canonical/launchpad/webapp/tales.py 2009-08-05 02:18:13 +0000 |
637 | @@ -2757,7 +2757,8 @@ |
638 | return text |
639 | result = ['<table class="diff">'] |
640 | |
641 | - for row, line in enumerate(text.split('\n')): |
642 | + max_format_lines = config.diff.max_format_lines |
643 | + for row, line in enumerate(text.splitlines()[:max_format_lines]): |
644 | result.append('<tr>') |
645 | result.append('<td class="line-no">%s</td>' % (row+1)) |
646 | if line.startswith('==='): |
647 | |
648 | === modified file 'lib/canonical/launchpad/webapp/tests/test_errorlog.py' |
649 | --- lib/canonical/launchpad/webapp/tests/test_errorlog.py 2009-07-17 00:26:05 +0000 |
650 | +++ lib/canonical/launchpad/webapp/tests/test_errorlog.py 2009-07-29 14:28:18 +0000 |
651 | @@ -57,8 +57,8 @@ |
652 | 'pageid', 'traceback-text', 'username', 'url', 42, |
653 | [('name1', 'value1'), ('name2', 'value2'), |
654 | ('name1', 'value3')], |
655 | - [(1, 5, 'SELECT 1'), |
656 | - (5, 10, 'SELECT 2')]) |
657 | + [(1, 5, 'store_a', 'SELECT 1'), |
658 | + (5, 10, 'store_b', 'SELECT 2')]) |
659 | self.assertEqual(entry.id, 'id') |
660 | self.assertEqual(entry.type, 'exc-type') |
661 | self.assertEqual(entry.value, 'exc-value') |
662 | @@ -74,8 +74,12 @@ |
663 | self.assertEqual(entry.req_vars[1], ('name2', 'value2')) |
664 | self.assertEqual(entry.req_vars[2], ('name1', 'value3')) |
665 | self.assertEqual(len(entry.db_statements), 2) |
666 | - self.assertEqual(entry.db_statements[0], (1, 5, 'SELECT 1')) |
667 | - self.assertEqual(entry.db_statements[1], (5, 10, 'SELECT 2')) |
668 | + self.assertEqual( |
669 | + entry.db_statements[0], |
670 | + (1, 5, 'store_a', 'SELECT 1')) |
671 | + self.assertEqual( |
672 | + entry.db_statements[1], |
673 | + (5, 10, 'store_b', 'SELECT 2')) |
674 | |
675 | def test_write(self): |
676 | """Test ErrorReport.write()""" |
677 | @@ -88,8 +92,8 @@ |
678 | [('HTTP_USER_AGENT', 'Mozilla/5.0'), |
679 | ('HTTP_REFERER', 'http://localhost:9000/'), |
680 | ('name=foo', 'hello\nworld')], |
681 | - [(1, 5, 'SELECT 1'), |
682 | - (5, 10, 'SELECT\n2')]) |
683 | + [(1, 5, 'store_a', 'SELECT 1'), |
684 | + (5, 10,'store_b', 'SELECT\n2')]) |
685 | fp = StringIO.StringIO() |
686 | entry.write(fp) |
687 | self.assertEqual(fp.getvalue(), dedent("""\ |
688 | @@ -108,13 +112,58 @@ |
689 | HTTP_REFERER=http://localhost:9000/ |
690 | name%%3Dfoo=hello%%0Aworld |
691 | |
692 | - 00001-00005 SELECT 1 |
693 | - 00005-00010 SELECT 2 |
694 | + 00001-00005@store_a SELECT 1 |
695 | + 00005-00010@store_b SELECT 2 |
696 | |
697 | traceback-text""" % (versioninfo.branch_nick, versioninfo.revno))) |
698 | |
699 | def test_read(self): |
700 | - """Test ErrorReport.read()""" |
701 | + """Test ErrorReport.read().""" |
702 | + fp = StringIO.StringIO(dedent("""\ |
703 | + Oops-Id: OOPS-A0001 |
704 | + Exception-Type: NotFound |
705 | + Exception-Value: error message |
706 | + Date: 2005-04-01T00:00:00+00:00 |
707 | + Page-Id: IFoo:+foo-template |
708 | + User: Sample User |
709 | + URL: http://localhost:9000/foo |
710 | + Duration: 42 |
711 | + |
712 | + HTTP_USER_AGENT=Mozilla/5.0 |
713 | + HTTP_REFERER=http://localhost:9000/ |
714 | + name%3Dfoo=hello%0Aworld |
715 | + |
716 | + 00001-00005@store_a SELECT 1 |
717 | + 00005-00010@store_b SELECT 2 |
718 | + |
719 | + traceback-text""")) |
720 | + entry = ErrorReport.read(fp) |
721 | + self.assertEqual(entry.id, 'OOPS-A0001') |
722 | + self.assertEqual(entry.type, 'NotFound') |
723 | + self.assertEqual(entry.value, 'error message') |
724 | + self.assertEqual(entry.time, datetime.datetime(2005, 4, 1)) |
725 | + self.assertEqual(entry.pageid, 'IFoo:+foo-template') |
726 | + self.assertEqual(entry.tb_text, 'traceback-text') |
727 | + self.assertEqual(entry.username, 'Sample User') |
728 | + self.assertEqual(entry.url, 'http://localhost:9000/foo') |
729 | + self.assertEqual(entry.duration, 42) |
730 | + self.assertEqual(len(entry.req_vars), 3) |
731 | + self.assertEqual(entry.req_vars[0], ('HTTP_USER_AGENT', |
732 | + 'Mozilla/5.0')) |
733 | + self.assertEqual(entry.req_vars[1], ('HTTP_REFERER', |
734 | + 'http://localhost:9000/')) |
735 | + self.assertEqual(entry.req_vars[2], ('name=foo', 'hello\nworld')) |
736 | + self.assertEqual(len(entry.db_statements), 2) |
737 | + self.assertEqual( |
738 | + entry.db_statements[0], |
739 | + (1, 5, 'store_a', 'SELECT 1')) |
740 | + self.assertEqual( |
741 | + entry.db_statements[1], |
742 | + (5, 10, 'store_b', 'SELECT 2')) |
743 | + |
744 | + |
745 | + def test_read_no_store_id(self): |
746 | + """Test ErrorReport.read() for old logs with no store_id.""" |
747 | fp = StringIO.StringIO(dedent("""\ |
748 | Oops-Id: OOPS-A0001 |
749 | Exception-Type: NotFound |
750 | @@ -137,8 +186,6 @@ |
751 | self.assertEqual(entry.id, 'OOPS-A0001') |
752 | self.assertEqual(entry.type, 'NotFound') |
753 | self.assertEqual(entry.value, 'error message') |
754 | - # XXX jamesh 2005-11-30: |
755 | - # this should probably convert back to a datetime |
756 | self.assertEqual(entry.time, datetime.datetime(2005, 4, 1)) |
757 | self.assertEqual(entry.pageid, 'IFoo:+foo-template') |
758 | self.assertEqual(entry.tb_text, 'traceback-text') |
759 | @@ -152,8 +199,8 @@ |
760 | 'http://localhost:9000/')) |
761 | self.assertEqual(entry.req_vars[2], ('name=foo', 'hello\nworld')) |
762 | self.assertEqual(len(entry.db_statements), 2) |
763 | - self.assertEqual(entry.db_statements[0], (1, 5, 'SELECT 1')) |
764 | - self.assertEqual(entry.db_statements[1], (5, 10, 'SELECT 2')) |
765 | + self.assertEqual(entry.db_statements[0], (1, 5, None, 'SELECT 1')) |
766 | + self.assertEqual(entry.db_statements[1], (5, 10, None, 'SELECT 2')) |
767 | |
768 | |
769 | class TestErrorReportingUtility(unittest.TestCase): |
770 | @@ -422,7 +469,7 @@ |
771 | # Test ErrorReportingUtility.raising() with an XML-RPC request. |
772 | request = TestRequest() |
773 | directlyProvides(request, IXMLRPCRequest) |
774 | - request.getPositionalArguments = lambda : (1,2) |
775 | + request.getPositionalArguments = lambda: (1, 2) |
776 | utility = ErrorReportingUtility() |
777 | now = datetime.datetime(2006, 04, 01, 00, 30, 00, tzinfo=UTC) |
778 | try: |
779 | @@ -469,7 +516,6 @@ |
780 | utility.raising(sys.exc_info(), request, now=now) |
781 | self.assertEqual(request.oopsid, None) |
782 | |
783 | - |
784 | def test_raising_for_script(self): |
785 | """Test ErrorReportingUtility.raising with a ScriptRequest.""" |
786 | utility = ErrorReportingUtility() |
787 | @@ -753,7 +799,7 @@ |
788 | # logged will have OOPS reports generated for them. |
789 | error_message = self.factory.getUniqueString() |
790 | try: |
791 | - 1/0 |
792 | + ignored = 1/0 |
793 | except ZeroDivisionError: |
794 | self.logger.exception(error_message) |
795 | oops_report = self.error_utility.getLastOopsReport() |
796 | |
797 | === modified file 'lib/canonical/launchpad/webapp/tests/test_tales.py' |
798 | --- lib/canonical/launchpad/webapp/tests/test_tales.py 2009-06-25 05:30:52 +0000 |
799 | +++ lib/canonical/launchpad/webapp/tests/test_tales.py 2009-08-05 01:06:49 +0000 |
800 | @@ -10,12 +10,13 @@ |
801 | from zope.security.proxy import removeSecurityProxy |
802 | from zope.testing.doctestunit import DocTestSuite |
803 | |
804 | +from canonical.config import config |
805 | from canonical.launchpad.ftests import test_tales |
806 | -from lp.testing import login, TestCase, TestCaseWithFactory |
807 | from canonical.launchpad.testing.pages import find_tags_by_class |
808 | from canonical.launchpad.webapp.tales import FormattersAPI |
809 | from canonical.testing import ( |
810 | DatabaseFunctionalLayer, LaunchpadFunctionalLayer) |
811 | +from lp.testing import login, TestCase, TestCaseWithFactory |
812 | |
813 | |
814 | def test_requestapi(): |
815 | @@ -209,6 +210,13 @@ |
816 | '<td class="text"> </td></tr></table>', |
817 | FormattersAPI(' ').format_diff()) |
818 | |
819 | + def test_format_unicode(self): |
820 | + # Sometimes the strings contain unicode, those should work too. |
821 | + self.assertEqual( |
822 | + u'<table class="diff"><tr><td class="line-no">1</td>' |
823 | + u'<td class="text">Unicode \u1010</td></tr></table>', |
824 | + FormattersAPI(u'Unicode \u1010').format_diff()) |
825 | + |
826 | def test_cssClasses(self): |
827 | # Different parts of the diff have different css classes. |
828 | diff = dedent('''\ |
829 | @@ -240,6 +248,25 @@ |
830 | 'diff-comment text'], |
831 | [str(tag['class']) for tag in text]) |
832 | |
833 | + def test_config_value_limits_line_count(self): |
834 | + # The config.diff.max_line_format contains the maximum number of lines |
835 | + # to format. |
836 | + diff = dedent('''\ |
837 | + === modified file 'tales.py' |
838 | + --- tales.py |
839 | + +++ tales.py |
840 | + @@ -2435,6 +2435,8 @@ |
841 | + def format_diff(self): |
842 | + - removed this line |
843 | + + added this line |
844 | + ######## |
845 | + # A merge directive comment. |
846 | + ''') |
847 | + self.pushConfig("diff", max_format_lines=3) |
848 | + html = FormattersAPI(diff).format_diff() |
849 | + line_count = html.count('<td class="line-no">') |
850 | + self.assertEqual(3, line_count) |
851 | + |
852 | |
853 | class TestPreviewDiffFormatter(TestCaseWithFactory): |
854 | """Test the PreviewDiffFormatterAPI class.""" |
855 | |
856 | === modified file 'lib/lp/bugs/browser/bugtask.py' |
857 | --- lib/lp/bugs/browser/bugtask.py 2009-07-24 10:11:20 +0000 |
858 | +++ lib/lp/bugs/browser/bugtask.py 2009-08-04 14:57:07 +0000 |
859 | @@ -2915,13 +2915,15 @@ |
860 | return self.context.isUserAffected(self.user) |
861 | |
862 | @property |
863 | - def affects_form_value(self): |
864 | - """The value to use in the inline me too form.""" |
865 | - affected = self.context.isUserAffected(self.user) |
866 | - if affected is None or affected == False: |
867 | - return 'YES' |
868 | + def current_user_affected_js_status(self): |
869 | + """A javascript literal indicating if the user is affected.""" |
870 | + affected = self.current_user_affected_status |
871 | + if affected is None: |
872 | + return 'null' |
873 | + elif affected: |
874 | + return 'true' |
875 | else: |
876 | - return 'NO' |
877 | + return 'false' |
878 | |
879 | |
880 | class BugTaskTableRowView(LaunchpadView): |
881 | |
882 | === modified file 'lib/lp/bugs/browser/tests/test_bugtask.py' |
883 | --- lib/lp/bugs/browser/tests/test_bugtask.py 2009-06-25 00:40:31 +0000 |
884 | +++ lib/lp/bugs/browser/tests/test_bugtask.py 2009-07-23 10:34:08 +0000 |
885 | @@ -1,23 +1,63 @@ |
886 | # Copyright 2009 Canonical Ltd. This software is licensed under the |
887 | # GNU Affero General Public License version 3 (see the file LICENSE). |
888 | |
889 | +__metaclass__ = type |
890 | + |
891 | + |
892 | import unittest |
893 | |
894 | from zope.testing.doctest import DocTestSuite |
895 | |
896 | -from lp.bugs.browser import bugtask |
897 | +from canonical.launchpad.ftests import login |
898 | from canonical.launchpad.testing.systemdocs import ( |
899 | LayeredDocFileSuite, setUp, tearDown) |
900 | from canonical.testing import LaunchpadFunctionalLayer |
901 | |
902 | +from lp.bugs.browser import bugtask |
903 | +from lp.bugs.browser.bugtask import BugTasksAndNominationsView |
904 | +from lp.testing import TestCaseWithFactory |
905 | + |
906 | + |
907 | +class TestBugTasksAndNominationsView(TestCaseWithFactory): |
908 | + |
909 | + layer = LaunchpadFunctionalLayer |
910 | + |
911 | + def setUp(self): |
912 | + super(TestBugTasksAndNominationsView, self).setUp() |
913 | + login('foo.bar@canonical.com') |
914 | + self.bug = self.factory.makeBug() |
915 | + self.view = BugTasksAndNominationsView(self.bug, None) |
916 | + |
917 | + def test_current_user_affected_status(self): |
918 | + self.failUnlessEqual( |
919 | + None, self.view.current_user_affected_status) |
920 | + self.view.context.markUserAffected(self.view.user, True) |
921 | + self.failUnlessEqual( |
922 | + True, self.view.current_user_affected_status) |
923 | + self.view.context.markUserAffected(self.view.user, False) |
924 | + self.failUnlessEqual( |
925 | + False, self.view.current_user_affected_status) |
926 | + |
927 | + def test_current_user_affected_js_status(self): |
928 | + self.failUnlessEqual( |
929 | + 'null', self.view.current_user_affected_js_status) |
930 | + self.view.context.markUserAffected(self.view.user, True) |
931 | + self.failUnlessEqual( |
932 | + 'true', self.view.current_user_affected_js_status) |
933 | + self.view.context.markUserAffected(self.view.user, False) |
934 | + self.failUnlessEqual( |
935 | + 'false', self.view.current_user_affected_js_status) |
936 | + |
937 | |
938 | def test_suite(): |
939 | suite = unittest.TestSuite() |
940 | + suite.addTest(unittest.makeSuite(TestBugTasksAndNominationsView)) |
941 | suite.addTest(DocTestSuite(bugtask)) |
942 | suite.addTest(LayeredDocFileSuite( |
943 | 'bugtask-target-link-titles.txt', setUp=setUp, tearDown=tearDown, |
944 | layer=LaunchpadFunctionalLayer)) |
945 | return suite |
946 | |
947 | + |
948 | if __name__ == '__main__': |
949 | unittest.TextTestRunner().run(test_suite()) |
950 | |
951 | === modified file 'lib/lp/bugs/doc/bugnotification-email.txt' |
952 | --- lib/lp/bugs/doc/bugnotification-email.txt 2009-06-12 16:36:02 +0000 |
953 | +++ lib/lp/bugs/doc/bugnotification-email.txt 2009-07-22 09:12:01 +0000 |
954 | @@ -6,7 +6,9 @@ |
955 | themselves; for that, see bugnotifications.txt. |
956 | |
957 | The reference spec associated with this document is available on the |
958 | -Launchpad wiki. <https://launchpad.canonical.com/FormattingBugNotifications> |
959 | +Launchpad development wiki: |
960 | + |
961 | + https://dev.launchpad.net/Bugs/Specs/FormattingBugNotifications |
962 | |
963 | You need to be logged in to edit bugs in Malone, so let's get started: |
964 | |
965 | |
966 | === modified file 'lib/lp/bugs/externalbugtracker/mantis.py' |
967 | --- lib/lp/bugs/externalbugtracker/mantis.py 2009-06-25 00:40:31 +0000 |
968 | +++ lib/lp/bugs/externalbugtracker/mantis.py 2009-07-22 09:12:01 +0000 |
969 | @@ -80,7 +80,9 @@ |
970 | """An `ExternalBugTracker` for dealing with Mantis instances. |
971 | |
972 | For a list of tested Mantis instances and their behaviour when |
973 | - exported from, see http://launchpad.canonical.com/MantisBugtrackers. |
974 | + exported from, see: |
975 | + |
976 | + https://dev.launchpad.net/Bugs/ExternalBugTrackers/Mantis |
977 | """ |
978 | |
979 | # Custom opener that automatically sends anonymous credentials to |
980 | |
981 | === modified file 'lib/lp/bugs/stories/bugs/xx-bug-affects-me-too.txt' |
982 | --- lib/lp/bugs/stories/bugs/xx-bug-affects-me-too.txt 2009-06-12 16:36:02 +0000 |
983 | +++ lib/lp/bugs/stories/bugs/xx-bug-affects-me-too.txt 2009-07-31 13:49:53 +0000 |
984 | @@ -1,7 +1,7 @@ |
985 | = Marking a bug as affecting the user = |
986 | |
987 | -Users can mark bugs as affecting them. Let's create a sample bug to try |
988 | -this on. |
989 | +Users can mark bugs as affecting them. Let's create a sample bug to |
990 | +try this out. |
991 | |
992 | >>> login(ANONYMOUS) |
993 | >>> from canonical.launchpad.webapp import canonical_url |
994 | @@ -9,15 +9,27 @@ |
995 | >>> test_bug_url = canonical_url(test_bug) |
996 | >>> logout() |
997 | |
998 | -The user goes to the bug's index page, and clicks the edit action link |
999 | -near 'This bug affects me too'. |
1000 | +The user goes to the bug's index page, and finds a statement that the |
1001 | +bug is not marked as affecting them. |
1002 | |
1003 | >>> user_browser.open(test_bug_url) |
1004 | >>> print extract_text(find_tag_by_id( |
1005 | - ... user_browser.contents, 'affectsmetooform')) |
1006 | - This bug doesn't affect me... |
1007 | - |
1008 | - >>> user_browser.getLink('change').click() |
1009 | + ... user_browser.contents, 'affectsmetoo').find( |
1010 | + ... None, 'static')) |
1011 | + This bug doesn't affect me |
1012 | + |
1013 | +Next to the statement is a link containing an edit icon. |
1014 | + |
1015 | + >>> edit_link = find_tag_by_id( |
1016 | + ... user_browser.contents, 'affectsmetoo').a |
1017 | + >>> print edit_link['href'] |
1018 | + +affectsmetoo |
1019 | + >>> print edit_link.img['src'] |
1020 | + /@@/edit |
1021 | + |
1022 | +The user is affected by this bug, so clicks the link. |
1023 | + |
1024 | + >>> user_browser.getLink(url='+affectsmetoo').click() |
1025 | >>> print user_browser.url |
1026 | http://bugs.launchpad.dev/.../+bug/.../+affectsmetoo |
1027 | >>> user_browser.getControl(name='field.affects').value |
1028 | @@ -28,22 +40,26 @@ |
1029 | >>> user_browser.getControl('Change').click() |
1030 | |
1031 | The bug page loads again, and now the text is changed, to make it |
1032 | -clear to the user that they can change the selection. |
1033 | +clear to the user that they have marked this bug as affecting them. |
1034 | |
1035 | - >>> print extract_text(find_tags_by_class( |
1036 | - ... user_browser.contents, 'menu-link-affectsmetoo')[0]) |
1037 | - change |
1038 | + >>> print extract_text(find_tag_by_id( |
1039 | + ... user_browser.contents, 'affectsmetoo').find( |
1040 | + ... None, 'static')) |
1041 | + This bug affects me too |
1042 | |
1043 | Next to it, we also see the 'hot bug' icon, to indicate that the user |
1044 | has marked the bug as affecting them. |
1045 | |
1046 | >>> print find_tag_by_id( |
1047 | - ... user_browser.contents, 'affectsmetooform').img['src'] |
1048 | + ... user_browser.contents, 'affectsmetoo').img['src'] |
1049 | /@@/flame-icon |
1050 | |
1051 | - >>> user_browser.getLink('change').click() |
1052 | - |
1053 | -The user is changing his selection to 'No' and submits the form. |
1054 | +On second thoughts, the user realises that this bug does not affect |
1055 | +them, so they click on the edit link once more. |
1056 | + |
1057 | + >>> user_browser.getLink(url='+affectsmetoo').click() |
1058 | + |
1059 | +The user changes his selection to 'No' and submits the form. |
1060 | |
1061 | >>> user_browser.getControl(name='field.affects').value = ['NO'] |
1062 | >>> user_browser.getControl('Change').click() |
1063 | @@ -51,18 +67,43 @@ |
1064 | Back at the bug page, the text changes once again. |
1065 | |
1066 | >>> print extract_text(find_tag_by_id( |
1067 | - ... user_browser.contents, 'affectsmetooform')) |
1068 | - This bug doesn't affect me... |
1069 | - |
1070 | -== One-click interaction == |
1071 | - |
1072 | -If the user's browser provides javascript, they don't need to go to |
1073 | -another page to change their selection. Instead, an in-page form is |
1074 | -submitted when they click the action link. |
1075 | - |
1076 | - >>> me_too_form = user_browser.getForm(id='affectsmetooform') |
1077 | - >>> user_browser.getControl(name='field.affects').value |
1078 | - 'YES' |
1079 | - >>> me_too_form.submit() |
1080 | - >>> user_browser.getControl(name='field.affects').value |
1081 | - 'NO' |
1082 | + ... user_browser.contents, 'affectsmetoo').find( |
1083 | + ... None, 'static')) |
1084 | + This bug doesn't affect me |
1085 | + |
1086 | + |
1087 | +== Static and dynamic support == |
1088 | + |
1089 | +A bug page contains markup to support both static (no Javascript) and |
1090 | +dynamic (Javascript enabled) scenarios. |
1091 | + |
1092 | + >>> def class_filter(css_class): |
1093 | + ... def test(node): |
1094 | + ... return css_class in node.get('class', '').split() |
1095 | + ... return test |
1096 | + |
1097 | + >>> static_content = find_tag_by_id( |
1098 | + ... user_browser.contents, 'affectsmetoo').find( |
1099 | + ... class_filter('static')) |
1100 | + |
1101 | + >>> static_content is not None |
1102 | + True |
1103 | + |
1104 | + >>> dynamic_content = find_tag_by_id( |
1105 | + ... user_browser.contents, 'affectsmetoo').find( |
1106 | + ... class_filter('dynamic')) |
1107 | + |
1108 | + >>> dynamic_content is not None |
1109 | + True |
1110 | + |
1111 | +The dynamic content is hidden by the presence of the "unseen" CSS |
1112 | +class. |
1113 | + |
1114 | + >>> print static_content.get('class') |
1115 | + static |
1116 | + |
1117 | + >>> print dynamic_content.get('class') |
1118 | + dynamic unseen |
1119 | + |
1120 | +It is the responsibilty of Javascript running in the page to unhide |
1121 | +the dynamic content and hide the static content. |
1122 | |
1123 | === modified file 'lib/lp/bugs/templates/bugtasks-and-nominations-table.pt' |
1124 | --- lib/lp/bugs/templates/bugtasks-and-nominations-table.pt 2009-07-17 17:59:07 +0000 |
1125 | +++ lib/lp/bugs/templates/bugtasks-and-nominations-table.pt 2009-08-04 14:51:23 +0000 |
1126 | @@ -38,11 +38,9 @@ |
1127 | |
1128 | </table> |
1129 | |
1130 | -<div |
1131 | - class="actions" |
1132 | - tal:define="current_bugtask view/current_bugtask" |
1133 | - |
1134 | - tal:condition="view/displayAlsoAffectsLinks"> |
1135 | +<div class="actions" |
1136 | + tal:define="current_bugtask view/current_bugtask" |
1137 | + tal:condition="view/displayAlsoAffectsLinks"> |
1138 | <tal:also-affects-links define="context_menu context/menu:context"> |
1139 | <tal:addupstream |
1140 | define="link context_menu/addupstream" |
1141 | @@ -56,56 +54,50 @@ |
1142 | define="link context_menu/nominate" |
1143 | condition="link/enabled" |
1144 | replace="structure link/render" /> |
1145 | - <form |
1146 | - id="affectsmetooform" |
1147 | - name="affectsmetooform" |
1148 | - method="post" |
1149 | - enctype="multipart/form-data" |
1150 | - accept-charset="UTF-8" |
1151 | - tal:define="link context_menu/affectsmetoo" |
1152 | - tal:condition="link/enabled" |
1153 | - tal:attributes="action link/url" |
1154 | - style="display: inline"> |
1155 | - <input |
1156 | - name="field.affects" |
1157 | - type="hidden" |
1158 | - tal:attributes="value view/affects_form_value" /> |
1159 | - <input |
1160 | - type="hidden" |
1161 | - name="field.actions.change" |
1162 | - value="" /> |
1163 | - <tal:affected condition="view/current_user_affected_status"> |
1164 | - <img width="14" height="14" src="/@@/flame-icon" alt="" /> |
1165 | - This bug affects me too |
1166 | - </tal:affected> |
1167 | - <tal:affected condition="not: view/current_user_affected_status"> |
1168 | - This bug doesn't affect me |
1169 | - </tal:affected> |
1170 | - (<tal:affectsmetoo |
1171 | - define="link context_menu/affectsmetoo" |
1172 | - condition="link/enabled" |
1173 | - replace="structure link/render" />) |
1174 | - <tal:nothing condition="nothing"> |
1175 | - The following is a trick to allow users to mark |
1176 | - themselves as affected by a bug with only one click. |
1177 | - If Javascript is available, we submit the form and |
1178 | - immediately go back to the same page, saving them the |
1179 | - need to go to a new page. |
1180 | - </tal:nothing> |
1181 | - <script type="text/javascript"> |
1182 | - function sendMeTooForm(e) { |
1183 | - $('affectsmetooform').submit(); |
1184 | - e.preventDefault(); |
1185 | - } |
1186 | - function connectMeTooLink() { |
1187 | - var me_too_link = getFirstElementByTagAndClassName( |
1188 | - 'a', 'menu-link-affectsmetoo'); |
1189 | - connect(me_too_link, 'onclick', sendMeTooForm); |
1190 | - } |
1191 | - registerLaunchpadFunction(connectMeTooLink); |
1192 | + <span id="affectsmetoo" style="display: inline" |
1193 | + tal:condition="link/enabled" |
1194 | + tal:define="link context_menu/affectsmetoo; |
1195 | + affected view/current_user_affected_status"> |
1196 | + |
1197 | + <tal:comment condition="nothing"> |
1198 | + This .static section is shown in browsers with javascript |
1199 | + enabled, and before setup_me_too is run. |
1200 | + </tal:comment> |
1201 | + <span class="static"> |
1202 | + <tal:affected condition="affected"> |
1203 | + <img width="14" height="14" src="/@@/flame-icon" alt="" /> |
1204 | + This bug affects me too |
1205 | + </tal:affected> |
1206 | + <tal:not-affected condition="not:affected"> |
1207 | + This bug doesn't affect me |
1208 | + </tal:not-affected> |
1209 | + <a href="+affectsmetoo"> |
1210 | + <img class="editicon" src="/@@/edit" alt="Edit" /> |
1211 | + </a> |
1212 | + </span> |
1213 | + |
1214 | + <tal:comment condition="nothing"> |
1215 | + This .dynamic section is used by setup_me_too to display |
1216 | + controls and information in the correct places. |
1217 | + </tal:comment> |
1218 | + <span class="dynamic unseen"> |
1219 | + <img src="/@@/flame-icon" alt=""/> |
1220 | + <a href="+affectsmetoo" class="js-action" |
1221 | + ><span class="value">Does this bug affect you?</span></a> |
1222 | + <img class="editicon" src="/@@/edit" alt="Edit" /> |
1223 | + </span> |
1224 | + |
1225 | + <script type="text/javascript" tal:content="string: |
1226 | + YUI().use('event', 'bugs.bugtask_index', function(Y) { |
1227 | + Y.on('load', function(e) { |
1228 | + Y.bugs.setup_me_too(${view/current_user_affected_js_status}); |
1229 | + }, window); |
1230 | + }); |
1231 | + "> |
1232 | </script> |
1233 | - </form> |
1234 | + |
1235 | + </span> |
1236 | </tal:also-affects-links> |
1237 | - |
1238 | </div> |
1239 | + |
1240 | </tal:root> |
1241 | |
1242 | === added file 'lib/lp/bugs/windmill/tests/test_bugs/test_bug_me_too.py' |
1243 | --- lib/lp/bugs/windmill/tests/test_bugs/test_bug_me_too.py 1970-01-01 00:00:00 +0000 |
1244 | +++ lib/lp/bugs/windmill/tests/test_bugs/test_bug_me_too.py 2009-07-31 15:27:05 +0000 |
1245 | @@ -0,0 +1,65 @@ |
1246 | +# Copyright 2009 Canonical Ltd. This software is licensed under the |
1247 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
1248 | + |
1249 | +from canonical.launchpad.windmill.testing import lpuser |
1250 | + |
1251 | +from windmill.authoring import WindmillTestClient |
1252 | + |
1253 | + |
1254 | +def test_me_too(): |
1255 | + """Test the "this bug affects me too" options on bug pages. |
1256 | + |
1257 | + This test ensures that, with Javascript enabled, the "me too" |
1258 | + status can be edited in-page. |
1259 | + """ |
1260 | + client = WindmillTestClient('Bug "me too" test') |
1261 | + lpuser.SAMPLE_PERSON.ensure_login(client) |
1262 | + |
1263 | + # Open bug 11 and wait for it to finish loading. |
1264 | + client.open(url=u'http://bugs.launchpad.dev:8085/jokosher/+bug/11/+index') |
1265 | + client.waits.forPageLoad(timeout=u'20000') |
1266 | + |
1267 | + # Wait for setup_me_too to sort out the "me too" elements. |
1268 | + client.waits.forElement( |
1269 | + xpath=(u"//span[@id='affectsmetoo' and " |
1270 | + u"@class='yui-metoocs-content']")) |
1271 | + |
1272 | + # Currently this bug does not affect the logged-in user. |
1273 | + client.asserts.assertText( |
1274 | + xpath=u"//span[@id='affectsmetoo']/span[@class='value']", |
1275 | + validator=u"This bug doesn't affect me") |
1276 | + |
1277 | + # There is an edit icon next to the text which can be clicked to |
1278 | + # edit the "me too" status. However, we won't click it with |
1279 | + # Windmill because the widget actually responds to mouse-down, and |
1280 | + # Windmill seems to do something funny instead. |
1281 | + client.mouseDown( |
1282 | + xpath=u"//span[@id='affectsmetoo']//img[@class='editicon']") |
1283 | + client.mouseUp( |
1284 | + xpath=u"//span[@id='affectsmetoo']//img[@class='editicon']") |
1285 | + |
1286 | + # Wait for the modal dialog to appear. |
1287 | + client.waits.forElement(id=u'yui-pretty-overlay-modal') |
1288 | + |
1289 | + # There's a close button if we change our mind. |
1290 | + client.click( |
1291 | + xpath=(u"//div[@id='yui-pretty-overlay-modal']//" |
1292 | + u"a[@class='close-button']")) |
1293 | + |
1294 | + # Wait for the modal dialog to disappear. Unfortunately the test |
1295 | + # below doesn't work, nor does testing clientWidth, or anything I |
1296 | + # could think of, so it's commented out for now because chasing |
1297 | + # this is not a good use of time. |
1298 | + |
1299 | + # client.asserts.assertElemJS( |
1300 | + # id=u'yui-pretty-overlay-modal', |
1301 | + # js=(u'getComputedStyle(element, ' |
1302 | + # u'"visibility").visibility == "hidden"')) |
1303 | + |
1304 | + # However, we want to mark this bug as affecting the logged-in |
1305 | + # user. We can also click on the content box of the "me too" |
1306 | + # widget; we are not forced to use the edit icon. |
1307 | + client.click(xpath=u"//span[@id='affectsmetoo']") |
1308 | + client.waits.forElement(id=u'yui-pretty-overlay-modal') |
1309 | + |
1310 | + # XXX: Gavin Panella bug=361097 2009-07-31: Finish this. |
1311 | |
1312 | === modified file 'lib/lp/code/browser/branch.py' |
1313 | --- lib/lp/code/browser/branch.py 2009-07-19 04:41:14 +0000 |
1314 | +++ lib/lp/code/browser/branch.py 2009-08-04 00:41:49 +0000 |
1315 | @@ -36,12 +36,15 @@ |
1316 | from zope.app.form.browser import TextAreaWidget |
1317 | from zope.traversing.interfaces import IPathAdapter |
1318 | from zope.component import getUtility, queryAdapter |
1319 | +from zope.event import notify |
1320 | from zope.formlib import form |
1321 | -from zope.interface import Interface, implements |
1322 | +from zope.interface import Interface, implements, providedBy |
1323 | from zope.publisher.interfaces import NotFound |
1324 | from zope.schema import Choice, Text |
1325 | from lazr.delegates import delegates |
1326 | from lazr.enum import EnumeratedType, Item |
1327 | +from lazr.lifecycle.event import ObjectModifiedEvent |
1328 | +from lazr.lifecycle.snapshot import Snapshot |
1329 | from lazr.uri import URI |
1330 | |
1331 | from canonical.cachedproperty import cachedproperty |
1332 | @@ -574,7 +577,6 @@ |
1333 | the user to be able to edit it. |
1334 | """ |
1335 | use_template(IBranch, include=[ |
1336 | - 'owner', |
1337 | 'name', |
1338 | 'url', |
1339 | 'description', |
1340 | @@ -582,6 +584,7 @@ |
1341 | 'whiteboard', |
1342 | ]) |
1343 | private = copy_field(IBranch['private'], readonly=False) |
1344 | + owner = copy_field(IBranch['owner'], readonly=False) |
1345 | |
1346 | |
1347 | class BranchEditFormView(LaunchpadEditFormView): |
1348 | @@ -598,9 +601,16 @@ |
1349 | @action('Change Branch', name='change') |
1350 | def change_action(self, action, data): |
1351 | # If the owner or product has changed, add an explicit notification. |
1352 | + # We take our own snapshot here to make sure that the snapshot records |
1353 | + # changes to the owner and private, and we notify the listeners |
1354 | + # explicitly below rather than the notification that would normally be |
1355 | + # sent in updateContextFromData. |
1356 | + branch_before_modification = Snapshot( |
1357 | + self.context, providing=providedBy(self.context)) |
1358 | if 'owner' in data: |
1359 | - new_owner = data['owner'] |
1360 | + new_owner = data.pop('owner') |
1361 | if new_owner != self.context.owner: |
1362 | + self.context.setOwner(new_owner, self.user) |
1363 | self.request.response.addNotification( |
1364 | "The branch owner has been changed to %s (%s)" |
1365 | % (new_owner.displayname, new_owner.name)) |
1366 | @@ -616,7 +626,13 @@ |
1367 | else: |
1368 | self.request.response.addNotification( |
1369 | "The branch is now publicly accessible.") |
1370 | - if self.updateContextFromData(data): |
1371 | + if self.updateContextFromData(data, notify_modified=False): |
1372 | + # Notify the object has changed with the snapshot that was taken |
1373 | + # earler. |
1374 | + field_names = [ |
1375 | + form_field.__name__ for form_field in self.form_fields] |
1376 | + notify(ObjectModifiedEvent( |
1377 | + self.context, branch_before_modification, field_names)) |
1378 | # Only specify that the context was modified if there |
1379 | # was in fact a change. |
1380 | self.context.date_last_modified = UTC_NOW |
1381 | |
1382 | === modified file 'lib/lp/code/browser/branchmergeproposal.py' |
1383 | --- lib/lp/code/browser/branchmergeproposal.py 2009-07-24 03:52:27 +0000 |
1384 | +++ lib/lp/code/browser/branchmergeproposal.py 2009-08-05 02:18:13 +0000 |
1385 | @@ -42,6 +42,7 @@ |
1386 | from lazr.lifecycle.event import ObjectModifiedEvent |
1387 | |
1388 | from canonical.cachedproperty import cachedproperty |
1389 | +from canonical.config import config |
1390 | |
1391 | from canonical.launchpad import _ |
1392 | from lp.code.adapters.branch import BranchMergeProposalDelta |
1393 | @@ -372,7 +373,7 @@ |
1394 | result.append(dict(style=style, comment=comment)) |
1395 | return result |
1396 | |
1397 | - @property |
1398 | + @cachedproperty |
1399 | def review_diff(self): |
1400 | """Return a (hopefully) intelligently encoded review diff.""" |
1401 | if self.context.review_diff is None: |
1402 | @@ -382,7 +383,25 @@ |
1403 | except UnicodeDecodeError: |
1404 | diff = self.context.review_diff.diff.text.decode('windows-1252', |
1405 | 'replace') |
1406 | - return diff |
1407 | + # Strip off the trailing carriage returns. |
1408 | + return diff.rstrip('\n') |
1409 | + |
1410 | + @cachedproperty |
1411 | + def review_diff_oversized(self): |
1412 | + """Return True if the review_diff is over the configured size limit. |
1413 | + |
1414 | + The diff can be over the limit in two ways. If the diff is oversized |
1415 | + in bytes it will be cut off at the Diff.text method. If the number of |
1416 | + lines is over the max_format_lines, then it is cut off at the fmt:diff |
1417 | + processing. |
1418 | + """ |
1419 | + review_diff = self.context.review_diff |
1420 | + if review_diff is None: |
1421 | + return False |
1422 | + if review_diff.diff.oversized: |
1423 | + return True |
1424 | + diff_text = self.review_diff |
1425 | + return diff_text.count('\n') >= config.diff.max_format_lines |
1426 | |
1427 | @property |
1428 | def has_bug_or_spec(self): |
1429 | |
1430 | === modified file 'lib/lp/code/browser/tests/test_branchmergeproposallisting.py' |
1431 | --- lib/lp/code/browser/tests/test_branchmergeproposallisting.py 2009-07-17 00:26:05 +0000 |
1432 | +++ lib/lp/code/browser/tests/test_branchmergeproposallisting.py 2009-08-02 23:25:37 +0000 |
1433 | @@ -8,14 +8,15 @@ |
1434 | from unittest import TestLoader |
1435 | |
1436 | import transaction |
1437 | +from zope.security.proxy import removeSecurityProxy |
1438 | |
1439 | +from canonical.launchpad.webapp.servers import LaunchpadTestRequest |
1440 | +from canonical.testing import DatabaseFunctionalLayer |
1441 | from lp.code.browser.branchmergeproposallisting import ( |
1442 | ActiveReviewsView, BranchMergeProposalListingView, PersonActiveReviewsView, |
1443 | ProductActiveReviewsView) |
1444 | from lp.code.enums import CodeReviewVote |
1445 | from lp.testing import ANONYMOUS, login, login_person, TestCaseWithFactory |
1446 | -from canonical.launchpad.webapp.servers import LaunchpadTestRequest |
1447 | -from canonical.testing import DatabaseFunctionalLayer |
1448 | |
1449 | _default = object() |
1450 | |
1451 | @@ -201,8 +202,7 @@ |
1452 | self.assertReviewGroupForUser( |
1453 | self.bmp.registrant, self._view.OTHER) |
1454 | team = self.factory.makeTeam(self.bmp.registrant) |
1455 | - login_person(self.bmp.source_branch.owner) |
1456 | - self.bmp.source_branch.owner = team |
1457 | + removeSecurityProxy(self.bmp.source_branch).owner = team |
1458 | self.assertReviewGroupForUser( |
1459 | self.bmp.registrant, ActiveReviewsView.MINE) |
1460 | |
1461 | |
1462 | === modified file 'lib/lp/code/configure.zcml' |
1463 | --- lib/lp/code/configure.zcml 2009-07-23 02:06:55 +0000 |
1464 | +++ lib/lp/code/configure.zcml 2009-07-30 00:30:02 +0000 |
1465 | @@ -430,9 +430,9 @@ |
1466 | "/> |
1467 | <require |
1468 | permission="launchpad.Edit" |
1469 | - attributes="destroySelf setPrivate" |
1470 | + attributes="destroySelf setPrivate setOwner setTarget" |
1471 | set_attributes="name url mirror_status_message |
1472 | - owner author description product lifecycle_status |
1473 | + description lifecycle_status |
1474 | last_mirrored last_mirrored_id last_mirror_attempt |
1475 | mirror_failures pull_disabled next_mirror_time |
1476 | last_scanned last_scanned_id revision_count branch_type |
1477 | |
1478 | === modified file 'lib/lp/code/doc/branch.txt' |
1479 | --- lib/lp/code/doc/branch.txt 2009-06-16 03:31:05 +0000 |
1480 | +++ lib/lp/code/doc/branch.txt 2009-08-02 23:59:31 +0000 |
1481 | @@ -116,9 +116,9 @@ |
1482 | >>> print new_branch.owner.name |
1483 | registrant |
1484 | |
1485 | -A user can create a branch where the owner is either themselves, |
1486 | -or a team that they are a member of. The registrant is not writable, |
1487 | -whereas the owner is. |
1488 | +A user can create a branch where the owner is either themselves, or a team |
1489 | +that they are a member of. Neither the owner nor the registrant are writable, |
1490 | +but the owner can be set using the `setOwner` method. |
1491 | |
1492 | >>> login('admin@canonical.com') |
1493 | >>> new_branch.registrant = factory.makePerson() |
1494 | @@ -126,7 +126,8 @@ |
1495 | ... |
1496 | ForbiddenAttribute: ('registrant', <Branch ...>) |
1497 | |
1498 | - >>> new_branch.owner = factory.makePerson(name='new-owner') |
1499 | + >>> team = factory.makeTeam(name='new-owner', owner=new_branch.owner) |
1500 | + >>> new_branch.setOwner(new_owner=team, user=new_branch.owner) |
1501 | >>> print new_branch.registrant.name |
1502 | registrant |
1503 | >>> print new_branch.owner.name |
1504 | |
1505 | === modified file 'lib/lp/code/interfaces/branch.py' |
1506 | --- lib/lp/code/interfaces/branch.py 2009-07-29 03:44:05 +0000 |
1507 | +++ lib/lp/code/interfaces/branch.py 2009-08-05 04:02:58 +0000 |
1508 | @@ -19,6 +19,7 @@ |
1509 | 'BranchCreatorNotMemberOfOwnerTeam', |
1510 | 'BranchCreatorNotOwner', |
1511 | 'BranchExists', |
1512 | + 'BranchTargetError', |
1513 | 'BranchTypeError', |
1514 | 'CannotDeleteBranch', |
1515 | 'DEFAULT_BRANCH_STATUS_IN_LISTING', |
1516 | @@ -90,7 +91,7 @@ |
1517 | """Raised when creating a branch that already exists.""" |
1518 | |
1519 | def __init__(self, existing_branch): |
1520 | - # XXX: JonathanLange 2008-12-04 spec=package-branches: This error |
1521 | + # XXX: TimPenhey 2009-07-12 bug=405214: This error |
1522 | # message logic is incorrect, but the exact text is being tested |
1523 | # in branch-xmlrpc.txt. |
1524 | params = {'name': existing_branch.name} |
1525 | @@ -108,6 +109,10 @@ |
1526 | BranchCreationException.__init__(self, message) |
1527 | |
1528 | |
1529 | +class BranchTargetError(Exception): |
1530 | + """Raised when there is an error determining a branch target.""" |
1531 | + |
1532 | + |
1533 | class CannotDeleteBranch(Exception): |
1534 | """The branch cannot be deleted at this time.""" |
1535 | |
1536 | @@ -402,14 +407,45 @@ |
1537 | title=_("The user that registered the branch."), |
1538 | required=True, readonly=True, |
1539 | vocabulary='ValidPersonOrTeam')) |
1540 | + |
1541 | owner = exported( |
1542 | ParticipatingPersonChoice( |
1543 | title=_('Owner'), |
1544 | - required=True, |
1545 | + required=True, readonly=True, |
1546 | vocabulary='UserTeamsParticipationPlusSelf', |
1547 | description=_("Either yourself or a team you are a member of. " |
1548 | "This controls who can modify the branch."))) |
1549 | |
1550 | + @call_with(user=REQUEST_USER) |
1551 | + @operation_parameters( |
1552 | + new_owner=Reference( |
1553 | + title=_("The new owner of the branch."), |
1554 | + schema=IPerson)) |
1555 | + @export_write_operation() |
1556 | + def setOwner(new_owner, user): |
1557 | + """Set the owner of the branch to be `new_owner`.""" |
1558 | + |
1559 | + @call_with(user=REQUEST_USER) |
1560 | + @operation_parameters( |
1561 | + project=Reference( |
1562 | + title=_("The project the branch belongs to."), |
1563 | + schema=Interface, required=False), # Really IProduct |
1564 | + source_package=Reference( |
1565 | + title=_("The source package the branch belongs to."), |
1566 | + schema=Interface, required=False)) # Really ISourcePackage |
1567 | + @export_write_operation() |
1568 | + def setTarget(user, project=None, source_package=None): |
1569 | + """Set the target of the branch to be `project` or `source_package`. |
1570 | + |
1571 | + Only one of `project` or `source_package` can be set, and if neither |
1572 | + is set, the branch gets moved into the junk namespace of the branch |
1573 | + owner. |
1574 | + |
1575 | + :raise: `BranchTargetError` if both project and source_package are set, |
1576 | + or if either the project or source_package fail to be adapted to an |
1577 | + IBranchTarget. |
1578 | + """ |
1579 | + |
1580 | reviewer = exported( |
1581 | PublicPersonChoice( |
1582 | title=_('Default Review Team'), |
1583 | @@ -456,7 +492,7 @@ |
1584 | product = exported( |
1585 | ReferenceChoice( |
1586 | title=_('Project'), |
1587 | - required=False, |
1588 | + required=False, readonly=True, |
1589 | vocabulary='Product', |
1590 | schema=Interface, |
1591 | description=_("The project this branch belongs to.")), |
1592 | |
1593 | === modified file 'lib/lp/code/interfaces/branchnamespace.py' |
1594 | --- lib/lp/code/interfaces/branchnamespace.py 2009-06-25 04:06:00 +0000 |
1595 | +++ lib/lp/code/interfaces/branchnamespace.py 2009-07-27 10:19:21 +0000 |
1596 | @@ -70,6 +70,24 @@ |
1597 | def isNameUsed(name): |
1598 | """Is 'name' already used in this namespace?""" |
1599 | |
1600 | + def moveBranch(branch, mover, new_name=None, rename_if_necessary=False): |
1601 | + """Move the branch into this namespace. |
1602 | + |
1603 | + :param branch: The `IBranch` to move. |
1604 | + :param mover: The `IPerson` doing the moving. |
1605 | + :param new_name: A new name for the branch. |
1606 | + :param rename_if_necessary: Rename the branch if the branch name |
1607 | + exists already in this namespace. |
1608 | + :raises BranchCreatorNotMemberOfOwnerTeam: if the namespace owner is |
1609 | + a team, and 'mover' is not in that team. |
1610 | + :raises BranchCreatorNotOwner: if the namespace owner is an individual |
1611 | + and 'mover' is not the owner. |
1612 | + :raises BranchCreationForbidden: if 'mover' is not allowed to create |
1613 | + a branch in this namespace due to privacy rules. |
1614 | + :raises BranchExists: if a branch with the 'name' exists already in |
1615 | + the namespace, and 'rename_if_necessary' is False. |
1616 | + """ |
1617 | + |
1618 | |
1619 | class IBranchNamespacePolicy(Interface): |
1620 | """Methods relating to branch creation and validation.""" |
1621 | @@ -133,11 +151,13 @@ |
1622 | validation constraints on IBranch.name. |
1623 | """ |
1624 | |
1625 | - def validateMove(branch, mover): |
1626 | + def validateMove(branch, mover, name=None): |
1627 | """Check that 'mover' can move 'branch' into this namespace. |
1628 | |
1629 | :param branch: An `IBranch` that might be moved. |
1630 | :param mover: The `IPerson` who would move it. |
1631 | + :param name: A new name for the branch. If None, the branch name is |
1632 | + used. |
1633 | :raises BranchCreatorNotMemberOfOwnerTeam: if the namespace owner is |
1634 | a team, and 'mover' is not in that team. |
1635 | :raises BranchCreatorNotOwner: if the namespace owner is an individual |
1636 | |
1637 | === modified file 'lib/lp/code/interfaces/diff.py' |
1638 | --- lib/lp/code/interfaces/diff.py 2009-06-25 04:06:00 +0000 |
1639 | +++ lib/lp/code/interfaces/diff.py 2009-08-04 23:07:33 +0000 |
1640 | @@ -27,7 +27,15 @@ |
1641 | class IDiff(Interface): |
1642 | """A diff that is stored in the Library.""" |
1643 | |
1644 | - text = Text(title=_('Textual contents of a diff.'), readonly=True) |
1645 | + text = Text( |
1646 | + title=_('Textual contents of a diff.'), readonly=True, |
1647 | + description=_("The text may be cut off at a defined maximum size.")) |
1648 | + |
1649 | + oversized = Bool( |
1650 | + readonly=True, |
1651 | + description=_( |
1652 | + "True if the size of the content is over the defined maximum " |
1653 | + "size.")) |
1654 | |
1655 | diff_text = exported( |
1656 | Bytes(title=_('Content of this diff'), required=True, readonly=True)) |
1657 | |
1658 | === modified file 'lib/lp/code/mail/branchmergeproposal.py' |
1659 | --- lib/lp/code/mail/branchmergeproposal.py 2009-07-22 18:37:32 +0000 |
1660 | +++ lib/lp/code/mail/branchmergeproposal.py 2009-08-05 02:18:13 +0000 |
1661 | @@ -193,6 +193,7 @@ |
1662 | 'gap': '', |
1663 | 'reviews': '', |
1664 | 'whiteboard': '', # No more whiteboard. |
1665 | + 'diff_cutoff_warning': '', |
1666 | } |
1667 | |
1668 | requested_reviews = [] |
1669 | @@ -212,6 +213,12 @@ |
1670 | params['comment'] = (self.comment.message.text_contents) |
1671 | if len(requested_reviews) > 0: |
1672 | params['gap'] = '\n\n' |
1673 | + |
1674 | + if (self.review_diff is not None and |
1675 | + self.review_diff.diff.oversized): |
1676 | + params['diff_cutoff_warning'] = ( |
1677 | + "The attached diff has been truncated due to its size.") |
1678 | + |
1679 | return params |
1680 | |
1681 | def _getTemplateParams(self, email): |
1682 | |
1683 | === modified file 'lib/lp/code/mail/tests/test_branchmergeproposal.py' |
1684 | --- lib/lp/code/mail/tests/test_branchmergeproposal.py 2009-07-22 18:37:32 +0000 |
1685 | +++ lib/lp/code/mail/tests/test_branchmergeproposal.py 2009-08-05 02:25:16 +0000 |
1686 | @@ -3,7 +3,7 @@ |
1687 | |
1688 | """Tests for BranchMergeProposal mailings""" |
1689 | |
1690 | -from unittest import TestLoader, TestCase |
1691 | +from unittest import TestLoader |
1692 | import transaction |
1693 | |
1694 | from zope.security.proxy import removeSecurityProxy |
1695 | @@ -22,20 +22,17 @@ |
1696 | from lp.code.model.branch import update_trigger_modified_fields |
1697 | from lp.code.model.codereviewvote import CodeReviewVoteReference |
1698 | from canonical.launchpad.webapp import canonical_url |
1699 | -from lp.testing import login, login_person, TestCaseWithFactory |
1700 | -from lp.testing.factory import LaunchpadObjectFactory |
1701 | +from lp.testing import login_person, TestCaseWithFactory |
1702 | from lp.testing.mail_helpers import pop_notifications |
1703 | |
1704 | |
1705 | -class TestMergeProposalMailing(TestCase): |
1706 | +class TestMergeProposalMailing(TestCaseWithFactory): |
1707 | """Test that reasonable mailings are generated""" |
1708 | |
1709 | layer = LaunchpadFunctionalLayer |
1710 | |
1711 | def setUp(self): |
1712 | - TestCase.setUp(self) |
1713 | - login('admin@canonical.com') |
1714 | - self.factory = LaunchpadObjectFactory() |
1715 | + super(TestMergeProposalMailing, self).setUp('admin@canonical.com') |
1716 | |
1717 | def makeProposalWithSubscriber(self, diff_text=None, |
1718 | initial_comment=None): |
1719 | @@ -182,6 +179,22 @@ |
1720 | attachment['Content-Disposition']) |
1721 | self.assertEqual('Fake diff', attachment.get_payload(decode=True)) |
1722 | |
1723 | + def test_generateEmail_attaches_diff_oversize_truncated(self): |
1724 | + """An oversized diff will be truncated, and the receiver informed.""" |
1725 | + self.pushConfig("diff", max_read_size=25) |
1726 | + content = "1234567890" * 10 |
1727 | + bmp, subscriber = self.makeProposalWithSubscriber( |
1728 | + diff_text=content) |
1729 | + mailer = BMPMailer.forCreation(bmp, bmp.registrant) |
1730 | + ctrl = mailer.generateEmail('baz.quxx@example.com', subscriber) |
1731 | + (attachment,) = ctrl.attachments |
1732 | + self.assertEqual('text/x-diff', attachment['Content-Type']) |
1733 | + self.assertEqual('inline; filename="review-diff.txt"', |
1734 | + attachment['Content-Disposition']) |
1735 | + self.assertEqual(content[:25], attachment.get_payload(decode=True)) |
1736 | + warning_text = "The attached diff has been truncated due to its size." |
1737 | + self.assertTrue(warning_text in ctrl.body) |
1738 | + |
1739 | def test_forModificationNoModification(self): |
1740 | """Ensure None is returned if no change has been made.""" |
1741 | merge_proposal, person = self.makeProposalWithSubscriber() |
1742 | |
1743 | === modified file 'lib/lp/code/model/branch.py' |
1744 | --- lib/lp/code/model/branch.py 2009-07-19 04:41:14 +0000 |
1745 | +++ lib/lp/code/model/branch.py 2009-07-31 01:07:04 +0000 |
1746 | @@ -57,7 +57,7 @@ |
1747 | from lp.code.event.branchmergeproposal import NewBranchMergeProposalEvent |
1748 | from lp.code.interfaces.branch import ( |
1749 | bazaar_identity, BranchCannotBePrivate, BranchCannotBePublic, |
1750 | - BranchTypeError, CannotDeleteBranch, |
1751 | + BranchTargetError, BranchTypeError, CannotDeleteBranch, |
1752 | DEFAULT_BRANCH_STATUS_IN_LISTING, IBranch, |
1753 | IBranchNavigationMenu, IBranchSet) |
1754 | from lp.code.interfaces.branchcollection import IAllBranches |
1755 | @@ -71,7 +71,7 @@ |
1756 | from lp.code.interfaces.seriessourcepackagebranch import ( |
1757 | IFindOfficialBranchLinks) |
1758 | from lp.registry.interfaces.person import ( |
1759 | - validate_person_not_private_membership, validate_public_person) |
1760 | + IPerson, validate_person_not_private_membership, validate_public_person) |
1761 | |
1762 | |
1763 | class Branch(SQLBase): |
1764 | @@ -113,6 +113,12 @@ |
1765 | owner = ForeignKey( |
1766 | dbName='owner', foreignKey='Person', |
1767 | storm_validator=validate_person_not_private_membership, notNull=True) |
1768 | + |
1769 | + def setOwner(self, new_owner, user): |
1770 | + """See `IBranch`.""" |
1771 | + new_namespace = self.target.getNamespace(new_owner) |
1772 | + new_namespace.moveBranch(self, user, rename_if_necessary=True) |
1773 | + |
1774 | reviewer = ForeignKey( |
1775 | dbName='reviewer', foreignKey='Person', |
1776 | storm_validator=validate_public_person, default=None) |
1777 | @@ -162,6 +168,28 @@ |
1778 | target = self.product |
1779 | return IBranchTarget(target) |
1780 | |
1781 | + def setTarget(self, user, project=None, source_package=None): |
1782 | + """See `IBranch`.""" |
1783 | + if project is not None: |
1784 | + if source_package is not None: |
1785 | + raise BranchTargetError( |
1786 | + 'Cannot specify both a project and a source package.') |
1787 | + else: |
1788 | + target = IBranchTarget(project) |
1789 | + if target is None: |
1790 | + raise BranchTargetError( |
1791 | + '%r is not a valid project target' % project) |
1792 | + elif source_package is not None: |
1793 | + target = IBranchTarget(source_package) |
1794 | + if target is None: |
1795 | + raise BranchTargetError( |
1796 | + '%r is not a valid source package target' % source_package) |
1797 | + else: |
1798 | + target = IBranchTarget(self.owner) |
1799 | + # Person targets are always valid. |
1800 | + namespace = target.getNamespace(self.owner) |
1801 | + namespace.moveBranch(self, user, rename_if_necessary=True) |
1802 | + |
1803 | @property |
1804 | def namespace(self): |
1805 | """See `IBranch`.""" |
1806 | |
1807 | === modified file 'lib/lp/code/model/branchnamespace.py' |
1808 | --- lib/lp/code/model/branchnamespace.py 2009-06-25 04:06:00 +0000 |
1809 | +++ lib/lp/code/model/branchnamespace.py 2009-08-04 00:41:49 +0000 |
1810 | @@ -16,6 +16,7 @@ |
1811 | from zope.component import getUtility |
1812 | from zope.event import notify |
1813 | from zope.interface import implements |
1814 | +from zope.security.proxy import removeSecurityProxy |
1815 | |
1816 | from lazr.lifecycle.event import ObjectCreatedEvent |
1817 | from storm.locals import And |
1818 | @@ -160,6 +161,25 @@ |
1819 | self.validateBranchName(name) |
1820 | self.validateRegistrant(mover) |
1821 | |
1822 | + def moveBranch(self, branch, mover, new_name=None, |
1823 | + rename_if_necessary=False): |
1824 | + """See `IBranchNamespace`.""" |
1825 | + # Check to see if the branch is already in this namespace. |
1826 | + old_namespace = branch.namespace |
1827 | + if self.name == old_namespace.name: |
1828 | + return |
1829 | + if new_name is None: |
1830 | + new_name = branch.name |
1831 | + if rename_if_necessary: |
1832 | + new_name = self.findUnusedName(new_name) |
1833 | + self.validateMove(branch, mover, new_name) |
1834 | + # Remove the security proxy of the branch as the owner and target |
1835 | + # attributes are readonly through the interface. |
1836 | + naked_branch = removeSecurityProxy(branch) |
1837 | + naked_branch.owner = self.owner |
1838 | + self.target._retargetBranch(naked_branch) |
1839 | + naked_branch.name = new_name |
1840 | + |
1841 | def createBranchWithPrefix(self, branch_type, prefix, registrant, |
1842 | url=None): |
1843 | """See `IBranchNamespace`.""" |
1844 | |
1845 | === modified file 'lib/lp/code/model/branchtarget.py' |
1846 | --- lib/lp/code/model/branchtarget.py 2009-07-17 00:26:05 +0000 |
1847 | +++ lib/lp/code/model/branchtarget.py 2009-08-04 00:41:49 +0000 |
1848 | @@ -13,7 +13,8 @@ |
1849 | |
1850 | from zope.component import getUtility |
1851 | from zope.interface import implements |
1852 | -from zope.security.proxy import isinstance as zope_isinstance |
1853 | +from zope.security.proxy import ( |
1854 | + removeSecurityProxy, isinstance as zope_isinstance) |
1855 | |
1856 | from lp.code.interfaces.branchcollection import IAllBranches |
1857 | from lp.code.interfaces.branchtarget import ( |
1858 | @@ -128,6 +129,16 @@ |
1859 | # those cases. |
1860 | return bug.default_bugtask |
1861 | |
1862 | + def _retargetBranch(self, branch): |
1863 | + """Set the branch target to refer to this target. |
1864 | + |
1865 | + This only updates the target related attributes of the branch, and |
1866 | + expects a branch without a security proxy as a parameter. |
1867 | + """ |
1868 | + branch.product = None |
1869 | + branch.distroseries = self.sourcepackage.distroseries |
1870 | + branch.sourcepackagename = self.sourcepackage.sourcepackagename |
1871 | + |
1872 | |
1873 | class PersonBranchTarget(_BaseBranchTarget): |
1874 | implements(IBranchTarget) |
1875 | @@ -182,6 +193,16 @@ |
1876 | """See `IBranchTarget`.""" |
1877 | return bug.default_bugtask |
1878 | |
1879 | + def _retargetBranch(self, branch): |
1880 | + """Set the branch target to refer to this target. |
1881 | + |
1882 | + This only updates the target related attributes of the branch, and |
1883 | + expects a branch without a security proxy as a parameter. |
1884 | + """ |
1885 | + branch.product = None |
1886 | + branch.distroseries = None |
1887 | + branch.sourcepackagename = None |
1888 | + |
1889 | |
1890 | class ProductBranchTarget(_BaseBranchTarget): |
1891 | implements(IBranchTarget) |
1892 | @@ -265,6 +286,16 @@ |
1893 | task = bug.bugtasks[0] |
1894 | return task |
1895 | |
1896 | + def _retargetBranch(self, branch): |
1897 | + """Set the branch target to refer to this target. |
1898 | + |
1899 | + This only updates the target related attributes of the branch, and |
1900 | + expects a branch without a security proxy as a parameter. |
1901 | + """ |
1902 | + branch.product = self.product |
1903 | + branch.distroseries = None |
1904 | + branch.sourcepackagename = None |
1905 | + |
1906 | |
1907 | def get_canonical_url_data_for_target(branch_target): |
1908 | """Return the `ICanonicalUrlData` for an `IBranchTarget`.""" |
1909 | |
1910 | === modified file 'lib/lp/code/model/diff.py' |
1911 | --- lib/lp/code/model/diff.py 2009-06-25 04:06:00 +0000 |
1912 | +++ lib/lp/code/model/diff.py 2009-08-04 23:07:33 +0000 |
1913 | @@ -15,8 +15,9 @@ |
1914 | from zope.component import getUtility |
1915 | from zope.interface import classProvides, implements |
1916 | |
1917 | +from canonical.config import config |
1918 | +from canonical.database.sqlbase import SQLBase |
1919 | from canonical.uuid import generate_uuid |
1920 | -from canonical.database.sqlbase import SQLBase |
1921 | |
1922 | from lp.code.interfaces.diff import ( |
1923 | IDiff, IPreviewDiff, IStaticDiff, IStaticDiffSource) |
1924 | @@ -45,10 +46,19 @@ |
1925 | else: |
1926 | self.diff_text.open() |
1927 | try: |
1928 | - return self.diff_text.read() |
1929 | + return self.diff_text.read(config.diff.max_read_size) |
1930 | finally: |
1931 | self.diff_text.close() |
1932 | |
1933 | + @property |
1934 | + def oversized(self): |
1935 | + # If the size of the content of the librarian file is over the |
1936 | + # config.diff.max_read_size, then we have an oversized diff. |
1937 | + if self.diff_text is None: |
1938 | + return False |
1939 | + diff_size = self.diff_text.content.filesize |
1940 | + return diff_size > config.diff.max_read_size |
1941 | + |
1942 | @classmethod |
1943 | def fromTrees(klass, from_tree, to_tree, filename=None): |
1944 | """Create a Diff from two Bazaar trees. |
1945 | |
1946 | === modified file 'lib/lp/code/model/tests/test_branch.py' |
1947 | --- lib/lp/code/model/tests/test_branch.py 2009-07-23 02:06:55 +0000 |
1948 | +++ lib/lp/code/model/tests/test_branch.py 2009-08-05 02:04:06 +0000 |
1949 | @@ -39,7 +39,8 @@ |
1950 | BranchVisibilityRule, CodeReviewNotificationLevel) |
1951 | from lp.code.interfaces.branch import ( |
1952 | BranchCannotBePrivate, BranchCannotBePublic, |
1953 | - CannotDeleteBranch, DEFAULT_BRANCH_STATUS_IN_LISTING) |
1954 | + BranchCreatorNotMemberOfOwnerTeam, BranchCreatorNotOwner, |
1955 | + BranchTargetError, CannotDeleteBranch, DEFAULT_BRANCH_STATUS_IN_LISTING) |
1956 | from lp.code.interfaces.branchlookup import IBranchLookup |
1957 | from lp.code.interfaces.branchnamespace import IBranchNamespaceSet |
1958 | from lp.code.interfaces.branchmergeproposal import InvalidBranchMergeProposal |
1959 | @@ -57,7 +58,6 @@ |
1960 | from lp.code.model.codeimport import CodeImport, CodeImportSet |
1961 | from lp.code.model.codereviewcomment import CodeReviewComment |
1962 | from lp.registry.interfaces.person import IPersonSet |
1963 | -from lp.registry.interfaces.product import IProductSet |
1964 | from lp.registry.model.product import ProductSet |
1965 | from lp.registry.model.sourcepackage import SourcePackage |
1966 | from lp.soyuz.interfaces.publishing import PackagePublishingPocket |
1967 | @@ -213,8 +213,7 @@ |
1968 | # attribute is updated too. |
1969 | branch = self.factory.makeAnyBranch() |
1970 | new_owner = self.factory.makePerson() |
1971 | - login('admin@canonical.com') |
1972 | - branch.owner = new_owner |
1973 | + removeSecurityProxy(branch).owner = new_owner |
1974 | # Call the function that is normally called through the event system |
1975 | # to auto reload the fields updated by the db triggers. |
1976 | update_trigger_modified_fields(branch) |
1977 | @@ -1054,9 +1053,9 @@ |
1978 | |
1979 | def setUp(self): |
1980 | TestCaseWithFactory.setUp(self, 'admin@canonical.com') |
1981 | - self.product = getUtility(IProductSet).getByName('firefox') |
1982 | + self.product = self.factory.makeProduct() |
1983 | |
1984 | - self.user = getUtility(IPersonSet).getByName('no-priv') |
1985 | + self.user = self.factory.makePerson() |
1986 | self.source = self.factory.makeProductBranch( |
1987 | name='source-branch', owner=self.user, product=self.product) |
1988 | self.target = self.factory.makeProductBranch( |
1989 | @@ -1069,7 +1068,7 @@ |
1990 | |
1991 | def test_junkSource(self): |
1992 | """Junk branches cannot be used as a source for merge proposals.""" |
1993 | - self.source.product = None |
1994 | + self.source.setTarget(user=self.source.owner) |
1995 | self.assertRaises( |
1996 | InvalidBranchMergeProposal, self.source.addLandingTarget, |
1997 | self.user, self.target) |
1998 | @@ -1078,12 +1077,13 @@ |
1999 | """The product of the target branch must match the product of the |
2000 | source branch. |
2001 | """ |
2002 | - self.target.product = None |
2003 | + self.target.setTarget(user=self.target.owner) |
2004 | self.assertRaises( |
2005 | InvalidBranchMergeProposal, self.source.addLandingTarget, |
2006 | self.user, self.target) |
2007 | |
2008 | - self.target.product = getUtility(IProductSet).getByName('bzr') |
2009 | + project = self.factory.makeProduct() |
2010 | + self.target.setTarget(user=self.target.owner, project=project) |
2011 | self.assertRaises( |
2012 | InvalidBranchMergeProposal, self.source.addLandingTarget, |
2013 | self.user, self.target) |
2014 | @@ -1097,12 +1097,13 @@ |
2015 | def test_dependentBranchSameProduct(self): |
2016 | """The dependent branch, if it is there, must be for the same product. |
2017 | """ |
2018 | - self.dependent.product = None |
2019 | + self.dependent.setTarget(user=self.dependent.owner) |
2020 | self.assertRaises( |
2021 | InvalidBranchMergeProposal, self.source.addLandingTarget, |
2022 | self.user, self.target, self.dependent) |
2023 | |
2024 | - self.dependent.product = getUtility(IProductSet).getByName('bzr') |
2025 | + project = self.factory.makeProduct() |
2026 | + self.dependent.setTarget(user=self.dependent.owner, project=project) |
2027 | self.assertRaises( |
2028 | InvalidBranchMergeProposal, self.source.addLandingTarget, |
2029 | self.user, self.target, self.dependent) |
2030 | @@ -1650,5 +1651,149 @@ |
2031 | self.assertEqual(branch.spec_links.count(), 0) |
2032 | |
2033 | |
2034 | +class TestBranchSetOwner(TestCaseWithFactory): |
2035 | + """Tests for IBranch.setOwner.""" |
2036 | + |
2037 | + layer = DatabaseFunctionalLayer |
2038 | + |
2039 | + def test_owner_sets_team(self): |
2040 | + # The owner of the branch can set the owner of the branch to be a team |
2041 | + # they are a member of. |
2042 | + branch = self.factory.makeAnyBranch() |
2043 | + team = self.factory.makeTeam(owner=branch.owner) |
2044 | + login_person(branch.owner) |
2045 | + branch.setOwner(team, branch.owner) |
2046 | + self.assertEqual(team, branch.owner) |
2047 | + |
2048 | + def test_owner_cannot_set_nonmember_team(self): |
2049 | + # The owner of the branch cannot set the owner to be a team they are |
2050 | + # not a member of. |
2051 | + branch = self.factory.makeAnyBranch() |
2052 | + team = self.factory.makeTeam() |
2053 | + login_person(branch.owner) |
2054 | + self.assertRaises( |
2055 | + BranchCreatorNotMemberOfOwnerTeam, |
2056 | + branch.setOwner, |
2057 | + team, branch.owner) |
2058 | + |
2059 | + def test_owner_cannot_set_other_user(self): |
2060 | + # The owner of the branch cannot set the new owner to be another |
2061 | + # person. |
2062 | + branch = self.factory.makeAnyBranch() |
2063 | + person = self.factory.makePerson() |
2064 | + login_person(branch.owner) |
2065 | + self.assertRaises( |
2066 | + BranchCreatorNotOwner, |
2067 | + branch.setOwner, |
2068 | + person, branch.owner) |
2069 | + |
2070 | + def test_admin_can_set_any_team_or_person(self): |
2071 | + # A Launchpad admin can set the branch to be owned by any team or |
2072 | + # person. |
2073 | + branch = self.factory.makeAnyBranch() |
2074 | + team = self.factory.makeTeam() |
2075 | + # To get a random administrator, choose the admin team owner. |
2076 | + admin = getUtility(ILaunchpadCelebrities).admin.teamowner |
2077 | + login_person(admin) |
2078 | + branch.setOwner(team, admin) |
2079 | + self.assertEqual(team, branch.owner) |
2080 | + person = self.factory.makePerson() |
2081 | + branch.setOwner(person, admin) |
2082 | + self.assertEqual(person, branch.owner) |
2083 | + |
2084 | + def test_bazaar_experts_can_set_any_team_or_person(self): |
2085 | + # A bazaar expert can set the branch to be owned by any team or |
2086 | + # person. |
2087 | + branch = self.factory.makeAnyBranch() |
2088 | + team = self.factory.makeTeam() |
2089 | + # To get a random administrator, choose the admin team owner. |
2090 | + experts = getUtility(ILaunchpadCelebrities).bazaar_experts.teamowner |
2091 | + login_person(experts) |
2092 | + branch.setOwner(team, experts) |
2093 | + self.assertEqual(team, branch.owner) |
2094 | + person = self.factory.makePerson() |
2095 | + branch.setOwner(person, experts) |
2096 | + self.assertEqual(person, branch.owner) |
2097 | + |
2098 | + |
2099 | +class TestBranchSetTarget(TestCaseWithFactory): |
2100 | + """Tests for IBranch.setTarget.""" |
2101 | + |
2102 | + layer = DatabaseFunctionalLayer |
2103 | + |
2104 | + def test_not_both_project_and_source_package(self): |
2105 | + # Only one of project or source_package can be passed in, not both. |
2106 | + branch = self.factory.makePersonalBranch() |
2107 | + project = self.factory.makeProduct() |
2108 | + source_package = self.factory.makeSourcePackage() |
2109 | + login_person(branch.owner) |
2110 | + self.assertRaises( |
2111 | + BranchTargetError, |
2112 | + branch.setTarget, |
2113 | + user=branch.owner, project=project, source_package=source_package) |
2114 | + |
2115 | + def test_junk_branch_to_project_branch(self): |
2116 | + # A junk branch can be moved to a project. |
2117 | + branch = self.factory.makePersonalBranch() |
2118 | + project = self.factory.makeProduct() |
2119 | + login_person(branch.owner) |
2120 | + branch.setTarget(user=branch.owner, project=project) |
2121 | + self.assertEqual(project, branch.target.context) |
2122 | + |
2123 | + def test_junk_branch_to_package_branch(self): |
2124 | + # A junk branch can be moved to a source package. |
2125 | + branch = self.factory.makePersonalBranch() |
2126 | + source_package = self.factory.makeSourcePackage() |
2127 | + login_person(branch.owner) |
2128 | + branch.setTarget(user=branch.owner, source_package=source_package) |
2129 | + self.assertEqual(source_package, branch.target.context) |
2130 | + |
2131 | + def test_project_branch_to_other_project_branch(self): |
2132 | + # Move a branch from one project to another. |
2133 | + branch = self.factory.makeProductBranch() |
2134 | + project = self.factory.makeProduct() |
2135 | + login_person(branch.owner) |
2136 | + branch.setTarget(user=branch.owner, project=project) |
2137 | + self.assertEqual(project, branch.target.context) |
2138 | + |
2139 | + def test_project_branch_to_package_branch(self): |
2140 | + # Move a branch from a project to a package. |
2141 | + branch = self.factory.makeProductBranch() |
2142 | + source_package = self.factory.makeSourcePackage() |
2143 | + login_person(branch.owner) |
2144 | + branch.setTarget(user=branch.owner, source_package=source_package) |
2145 | + self.assertEqual(source_package, branch.target.context) |
2146 | + |
2147 | + def test_project_branch_to_junk_branch(self): |
2148 | + # Move a branch from a project to junk. |
2149 | + branch = self.factory.makeProductBranch() |
2150 | + login_person(branch.owner) |
2151 | + branch.setTarget(user=branch.owner) |
2152 | + self.assertEqual(branch.owner, branch.target.context) |
2153 | + |
2154 | + def test_package_branch_to_other_package_branch(self): |
2155 | + # Move a branch from one package to another. |
2156 | + branch = self.factory.makePackageBranch() |
2157 | + source_package = self.factory.makeSourcePackage() |
2158 | + login_person(branch.owner) |
2159 | + branch.setTarget(user=branch.owner, source_package=source_package) |
2160 | + self.assertEqual(source_package, branch.target.context) |
2161 | + |
2162 | + def test_package_branch_to_project_branch(self): |
2163 | + # Move a branch from a package to a project. |
2164 | + branch = self.factory.makePackageBranch() |
2165 | + project = self.factory.makeProduct() |
2166 | + login_person(branch.owner) |
2167 | + branch.setTarget(user=branch.owner, project=project) |
2168 | + self.assertEqual(project, branch.target.context) |
2169 | + |
2170 | + def test_package_branch_to_junk_branch(self): |
2171 | + # Move a branch from a package to junk. |
2172 | + branch = self.factory.makePackageBranch() |
2173 | + login_person(branch.owner) |
2174 | + branch.setTarget(user=branch.owner) |
2175 | + self.assertEqual(branch.owner, branch.target.context) |
2176 | + |
2177 | + |
2178 | def test_suite(): |
2179 | return TestLoader().loadTestsFromName(__name__) |
2180 | |
2181 | === renamed file 'lib/lp/code/tests/test_branchnamespace.py' => 'lib/lp/code/model/tests/test_branchnamespace.py' |
2182 | --- lib/lp/code/tests/test_branchnamespace.py 2009-06-25 04:06:00 +0000 |
2183 | +++ lib/lp/code/model/tests/test_branchnamespace.py 2009-07-29 02:16:43 +0000 |
2184 | @@ -1871,5 +1871,70 @@ |
2185 | BranchCreatorNotOwner, self.albert, self.doug) |
2186 | |
2187 | |
2188 | +class TestBranchNamespaceMoveBranch(TestCaseWithFactory): |
2189 | + """Test the IBranchNamespace.moveBranch method. |
2190 | + |
2191 | + The edge cases of the validateMove are tested in the NamespaceMixin for |
2192 | + each of the namespaces. |
2193 | + """ |
2194 | + |
2195 | + layer = DatabaseFunctionalLayer |
2196 | + |
2197 | + def assertNamespacesEqual(self, expected, result): |
2198 | + """Assert that the namespaces refer to the same thing. |
2199 | + |
2200 | + The name of the namespace contains the user name and the context |
2201 | + parts, so is the easiest thing to check. |
2202 | + """ |
2203 | + self.assertEqual(expected.name, result.name) |
2204 | + |
2205 | + def test_move_to_same_namespace(self): |
2206 | + # Moving to the same namespace is effectively a no-op. No exceptions |
2207 | + # about matching branch names should be raised. |
2208 | + branch = self.factory.makeAnyBranch() |
2209 | + namespace = branch.namespace |
2210 | + namespace.moveBranch(branch, branch.owner) |
2211 | + self.assertNamespacesEqual(namespace, branch.namespace) |
2212 | + |
2213 | + def test_name_clash_raises(self): |
2214 | + # A name clash will raise an exception. |
2215 | + branch = self.factory.makeAnyBranch(name="test") |
2216 | + another = self.factory.makeAnyBranch(owner=branch.owner, name="test") |
2217 | + namespace = another.namespace |
2218 | + self.assertRaises( |
2219 | + BranchExists, namespace.moveBranch, branch, branch.owner) |
2220 | + |
2221 | + def test_move_with_rename(self): |
2222 | + # A name clash with 'rename_if_necessary' set to True will cause the |
2223 | + # branch to be renamed instead of raising an error. |
2224 | + branch = self.factory.makeAnyBranch(name="test") |
2225 | + another = self.factory.makeAnyBranch(owner=branch.owner, name="test") |
2226 | + namespace = another.namespace |
2227 | + namespace.moveBranch(branch, branch.owner, rename_if_necessary=True) |
2228 | + self.assertEqual("test-1", branch.name) |
2229 | + self.assertNamespacesEqual(namespace, branch.namespace) |
2230 | + |
2231 | + def test_move_with_new_name(self): |
2232 | + # A new name for the branch can be specified as part of the move. |
2233 | + branch = self.factory.makeAnyBranch(name="test") |
2234 | + another = self.factory.makeAnyBranch(owner=branch.owner, name="test") |
2235 | + namespace = another.namespace |
2236 | + namespace.moveBranch(branch, branch.owner, new_name="foo") |
2237 | + self.assertEqual("foo", branch.name) |
2238 | + self.assertNamespacesEqual(namespace, branch.namespace) |
2239 | + |
2240 | + def test_sets_branch_owner(self): |
2241 | + # Moving to a new namespace may change the owner of the branch if the |
2242 | + # owner of the namespace is different. |
2243 | + branch = self.factory.makeAnyBranch(name="test") |
2244 | + team = self.factory.makeTeam(branch.owner) |
2245 | + product = self.factory.makeProduct() |
2246 | + namespace = ProductNamespace(team, product) |
2247 | + namespace.moveBranch(branch, branch.owner) |
2248 | + self.assertEqual(team, branch.owner) |
2249 | + # And for paranoia. |
2250 | + self.assertNamespacesEqual(namespace, branch.namespace) |
2251 | + |
2252 | + |
2253 | def test_suite(): |
2254 | return unittest.TestLoader().loadTestsFromName(__name__) |
2255 | |
2256 | === modified file 'lib/lp/code/model/tests/test_branchtarget.py' |
2257 | --- lib/lp/code/model/tests/test_branchtarget.py 2009-06-25 04:06:00 +0000 |
2258 | +++ lib/lp/code/model/tests/test_branchtarget.py 2009-08-04 00:41:49 +0000 |
2259 | @@ -46,6 +46,24 @@ |
2260 | branches = self.target.collection.getBranches() |
2261 | self.assertEqual([branch], list(branches)) |
2262 | |
2263 | + def test_retargetBranch_packageBranch(self): |
2264 | + # Retarget an existing package branch to this target. |
2265 | + branch = self.factory.makePackageBranch() |
2266 | + self.target._retargetBranch(removeSecurityProxy(branch)) |
2267 | + self.assertEqual(self.target, branch.target) |
2268 | + |
2269 | + def test_retargetBranch_productBranch(self): |
2270 | + # Retarget an existing product branch to this target. |
2271 | + branch = self.factory.makeProductBranch() |
2272 | + self.target._retargetBranch(removeSecurityProxy(branch)) |
2273 | + self.assertEqual(self.target, branch.target) |
2274 | + |
2275 | + def test_retargetBranch_personalBranch(self): |
2276 | + # Retarget an existing personal branch to this target. |
2277 | + branch = self.factory.makePersonalBranch() |
2278 | + self.target._retargetBranch(removeSecurityProxy(branch)) |
2279 | + self.assertEqual(self.target, branch.target) |
2280 | + |
2281 | |
2282 | class TestPackageBranchTarget(TestCaseWithFactory, BaseBranchTargetTests): |
2283 | |
2284 | @@ -212,6 +230,33 @@ |
2285 | # The default merge target is always None. |
2286 | self.assertIs(None, self.target.default_merge_target) |
2287 | |
2288 | + def test_retargetBranch_packageBranch(self): |
2289 | + # Retarget an existing package branch to this target. Override the |
2290 | + # mixin tests, and specify the owner of the branch. This is needed to |
2291 | + # match the target as the target is the branch owner for a personal |
2292 | + # branch. |
2293 | + branch = self.factory.makePackageBranch(owner=self.original) |
2294 | + self.target._retargetBranch(removeSecurityProxy(branch)) |
2295 | + self.assertEqual(self.target, branch.target) |
2296 | + |
2297 | + def test_retargetBranch_productBranch(self): |
2298 | + # Retarget an existing product branch to this target. Override the |
2299 | + # mixin tests, and specify the owner of the branch. This is needed to |
2300 | + # match the target as the target is the branch owner for a personal |
2301 | + # branch. |
2302 | + branch = self.factory.makeProductBranch(owner=self.original) |
2303 | + self.target._retargetBranch(removeSecurityProxy(branch)) |
2304 | + self.assertEqual(self.target, branch.target) |
2305 | + |
2306 | + def test_retargetBranch_personalBranch(self): |
2307 | + # Retarget an existing personal branch to this target. Override the |
2308 | + # mixin tests, and specify the owner of the branch. This is needed to |
2309 | + # match the target as the target is the branch owner for a personal |
2310 | + # branch. |
2311 | + branch = self.factory.makePersonalBranch(owner=self.original) |
2312 | + self.target._retargetBranch(removeSecurityProxy(branch)) |
2313 | + self.assertEqual(self.target, branch.target) |
2314 | + |
2315 | |
2316 | class TestProductBranchTarget(TestCaseWithFactory, BaseBranchTargetTests): |
2317 | |
2318 | |
2319 | === modified file 'lib/lp/code/model/tests/test_diff.py' |
2320 | --- lib/lp/code/model/tests/test_diff.py 2009-06-25 04:06:00 +0000 |
2321 | +++ lib/lp/code/model/tests/test_diff.py 2009-08-04 23:07:33 +0000 |
2322 | @@ -6,27 +6,66 @@ |
2323 | __metaclass__ = type |
2324 | |
2325 | |
2326 | +from cStringIO import StringIO |
2327 | from unittest import TestLoader |
2328 | |
2329 | -from canonical.testing import ( |
2330 | - DatabaseFunctionalLayer, LaunchpadFunctionalLayer, LaunchpadZopelessLayer) |
2331 | import transaction |
2332 | |
2333 | +from canonical.launchpad.webapp import canonical_url |
2334 | +from canonical.launchpad.webapp.testing import verifyObject |
2335 | +from canonical.testing import LaunchpadFunctionalLayer, LaunchpadZopelessLayer |
2336 | from lp.code.model.diff import Diff, StaticDiff |
2337 | from lp.code.interfaces.diff import ( |
2338 | IDiff, IPreviewDiff, IStaticDiff, IStaticDiffSource) |
2339 | from lp.testing import login, login_person, TestCaseWithFactory |
2340 | -from canonical.launchpad.webapp import canonical_url |
2341 | -from canonical.launchpad.webapp.testing import verifyObject |
2342 | |
2343 | |
2344 | class TestDiff(TestCaseWithFactory): |
2345 | |
2346 | - layer = DatabaseFunctionalLayer |
2347 | + layer = LaunchpadFunctionalLayer |
2348 | |
2349 | def test_providesInterface(self): |
2350 | verifyObject(IDiff, Diff()) |
2351 | |
2352 | + def _create_diff(self, content): |
2353 | + # Create a Diff object with the content specified. |
2354 | + sio = StringIO() |
2355 | + sio.write(content) |
2356 | + size = sio.tell() |
2357 | + sio.seek(0) |
2358 | + diff = Diff.fromFile(sio, size) |
2359 | + # Commit to make the alias available for reading. |
2360 | + transaction.commit() |
2361 | + return diff |
2362 | + |
2363 | + def test_text_reads_librarian_content(self): |
2364 | + # IDiff.text will read at most config.diff.max_read_size bytes from |
2365 | + # the librarian. |
2366 | + content = "1234567890" * 10 |
2367 | + diff = self._create_diff(content) |
2368 | + self.assertEqual(content, diff.text) |
2369 | + |
2370 | + def test_oversized_normal(self): |
2371 | + # A diff smaller than config.diff.max_read_size is not oversized. |
2372 | + content = "1234567890" * 10 |
2373 | + diff = self._create_diff(content) |
2374 | + self.assertFalse(diff.oversized) |
2375 | + |
2376 | + def test_text_read_limited_by_config(self): |
2377 | + # IDiff.text will read at most config.diff.max_read_size bytes from |
2378 | + # the librarian. |
2379 | + self.pushConfig("diff", max_read_size=25) |
2380 | + content = "1234567890" * 10 |
2381 | + diff = self._create_diff(content) |
2382 | + self.assertEqual(content[:25], diff.text) |
2383 | + |
2384 | + def test_oversized_for_big_diff(self): |
2385 | + # A diff larger than config.diff.max_read_size is oversized. |
2386 | + self.pushConfig("diff", max_read_size=25) |
2387 | + content = "1234567890" * 10 |
2388 | + diff = self._create_diff(content) |
2389 | + self.assertTrue(diff.oversized) |
2390 | + |
2391 | |
2392 | class TestStaticDiff(TestCaseWithFactory): |
2393 | """Test that StaticDiff objects work.""" |
2394 | |
2395 | === modified file 'lib/lp/code/model/tests/test_revision.py' |
2396 | --- lib/lp/code/model/tests/test_revision.py 2009-06-25 04:06:00 +0000 |
2397 | +++ lib/lp/code/model/tests/test_revision.py 2009-08-02 23:17:10 +0000 |
2398 | @@ -146,7 +146,8 @@ |
2399 | branch.createBranchRevision(1, rev) |
2400 | # Once the branch is connected to the revision, we now specify |
2401 | # a product for the branch. |
2402 | - branch.product = self.factory.makeProduct() |
2403 | + project = self.factory.makeProduct() |
2404 | + branch.setTarget(user=branch.owner, project=project) |
2405 | # The revision is now identified as needing karma allocated. |
2406 | self.assertEqual( |
2407 | [rev], list(RevisionSet.getRevisionsNeedingKarmaAllocated())) |
2408 | |
2409 | === modified file 'lib/lp/code/scripts/tests/test_revisionkarma.py' |
2410 | --- lib/lp/code/scripts/tests/test_revisionkarma.py 2009-06-25 04:06:00 +0000 |
2411 | +++ lib/lp/code/scripts/tests/test_revisionkarma.py 2009-08-04 00:41:49 +0000 |
2412 | @@ -49,7 +49,8 @@ |
2413 | branch.createBranchRevision(1, rev) |
2414 | # Once the branch is connected to the revision, we now specify |
2415 | # a product for the branch. |
2416 | - branch.product = self.factory.makeProduct() |
2417 | + project = self.factory.makeProduct() |
2418 | + branch.setTarget(user=branch.owner, project=project) |
2419 | # Commit and switch to the script db user. |
2420 | transaction.commit() |
2421 | LaunchpadZopelessLayer.switchDbUser(config.revisionkarma.dbuser) |
2422 | |
2423 | === modified file 'lib/lp/code/templates/branchmergeproposal-index.pt' |
2424 | --- lib/lp/code/templates/branchmergeproposal-index.pt 2009-08-04 05:02:41 +0000 |
2425 | +++ lib/lp/code/templates/branchmergeproposal-index.pt 2009-08-05 02:18:13 +0000 |
2426 | @@ -139,6 +139,10 @@ |
2427 | <div class="boardCommentBody attachmentBody"> |
2428 | <tal:diff replace="structure view/review_diff/fmt:diff" /> |
2429 | </div> |
2430 | + <div class="boardCommentFooter" |
2431 | + tal:condition="view/review_diff_oversized"> |
2432 | + The diff has been truncated for viewing. |
2433 | + </div> |
2434 | </tal:real-diff> |
2435 | <tal:empty-diff condition="not: attachment"> |
2436 | <div class="boardCommentBody attachmentBody"> |
2437 | |
2438 | === modified file 'lib/lp/codehosting/tests/test_acceptance.py' |
2439 | --- lib/lp/codehosting/tests/test_acceptance.py 2009-07-23 18:10:26 +0000 |
2440 | +++ lib/lp/codehosting/tests/test_acceptance.py 2009-08-04 00:41:49 +0000 |
2441 | @@ -382,7 +382,7 @@ |
2442 | # rename as far as bzr is concerned: the URL changes. |
2443 | LaunchpadZopelessTestSetup().txn.begin() |
2444 | branch = self.getDatabaseBranch('testuser', None, 'test-branch') |
2445 | - branch.product = Product.byName('firefox') |
2446 | + branch.setTarget(user=branch.owner, project=Product.byName('firefox')) |
2447 | LaunchpadZopelessTestSetup().txn.commit() |
2448 | |
2449 | self.assertNotBranch( |
2450 | |
2451 | === modified file 'lib/lp/registry/browser/tests/private-team-creation-views.txt' |
2452 | --- lib/lp/registry/browser/tests/private-team-creation-views.txt 2009-07-31 02:45:01 +0000 |
2453 | +++ lib/lp/registry/browser/tests/private-team-creation-views.txt 2009-08-03 21:46:09 +0000 |
2454 | @@ -400,8 +400,7 @@ |
2455 | ... bugtask = bug.default_bugtask |
2456 | ... bugtask.transitionToAssignee(team) |
2457 | ... # A branch. |
2458 | - ... branch = factory.makeBranch() |
2459 | - ... branch.owner = team |
2460 | + ... branch = factory.makeBranch(owner=team, registrant=team_owner) |
2461 | ... # A branch subscription. |
2462 | ... from lp.code.enums import ( |
2463 | ... BranchSubscriptionDiffSize, |
2464 | |
2465 | === modified file 'lib/lp/registry/doc/private-team-roles.txt' |
2466 | --- lib/lp/registry/doc/private-team-roles.txt 2009-07-31 02:45:01 +0000 |
2467 | +++ lib/lp/registry/doc/private-team-roles.txt 2009-08-03 01:41:07 +0000 |
2468 | @@ -23,9 +23,10 @@ |
2469 | ----------------- |
2470 | |
2471 | >>> # Create the necessary teams. |
2472 | - >>> from lp.registry.interfaces.person import PersonVisibility |
2473 | >>> team_owner = factory.makePerson(name='team-owner') |
2474 | - >>> login('foo.bar@canonical.com') |
2475 | + >>> from lp.registry.interfaces.person import IPersonSet, PersonVisibility |
2476 | + >>> admin_user = getUtility(IPersonSet).getByEmail('admin@canonical.com') |
2477 | + >>> login_person(admin_user) |
2478 | >>> priv_team = factory.makeTeam(name='private-team', |
2479 | ... owner=team_owner, |
2480 | ... visibility=PersonVisibility.PRIVATE) |
2481 | @@ -102,12 +103,12 @@ |
2482 | Private teams can be assigned as the owner of a branch |
2483 | |
2484 | >>> branch = factory.makeBranch() |
2485 | - >>> branch.owner = priv_team |
2486 | + >>> branch.setOwner(priv_team, user=admin_user) |
2487 | |
2488 | But private membership teams cannot own a branch. |
2489 | |
2490 | >>> branch = factory.makeBranch() |
2491 | - >>> branch.owner = pm_team |
2492 | + >>> branch.setOwner(pm_team, user=admin_user) |
2493 | Traceback (most recent call last): |
2494 | ... |
2495 | PrivatePersonLinkageError: Cannot link person |
2496 | |
2497 | === modified file 'lib/lp/testing/factory.py' |
2498 | --- lib/lp/testing/factory.py 2009-07-23 17:47:50 +0000 |
2499 | +++ lib/lp/testing/factory.py 2009-08-03 21:46:09 +0000 |
2500 | @@ -600,7 +600,10 @@ |
2501 | distroseries = sourcepackage.distroseries |
2502 | |
2503 | if registrant is None: |
2504 | - registrant = owner |
2505 | + if owner.is_team: |
2506 | + registrant = owner.teamowner |
2507 | + else: |
2508 | + registrant = owner |
2509 | |
2510 | if branch_type in (BranchType.HOSTED, BranchType.IMPORTED): |
2511 | url = None |
2512 | |
2513 | === modified file 'lib/lp/translations/scripts/message_sharing_migration.py' |
2514 | --- lib/lp/translations/scripts/message_sharing_migration.py 2009-07-19 04:41:14 +0000 |
2515 | +++ lib/lp/translations/scripts/message_sharing_migration.py 2009-08-05 10:45:31 +0000 |
2516 | @@ -309,8 +309,8 @@ |
2517 | message = removeSecurityProxy(message) |
2518 | |
2519 | clashing_current, clashing_imported, twin = ( |
2520 | - self._findClashesFromDicts( |
2521 | - existing_tms, current_tms, imported_tms, message)) |
2522 | + self._findClashes( |
2523 | + message, representative, message.potemplate)) |
2524 | |
2525 | if clashing_current or clashing_imported: |
2526 | saved = self._saveByDiverging( |
= 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