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

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

= Bug 403992 =

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

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

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

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

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

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

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

Jeroen

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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(

Subscribers

People subscribed via source and target branches

to status/vote changes: