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

Proposed by Jeroen T. Vermeulen
Status: Rejected
Rejected by: Jeroen T. Vermeulen
Proposed branch: lp:~jtv/launchpad/bug-427278
Merge into: lp:launchpad/db-devel
Diff against target: 2165 lines (+938/-380)
34 files modified
database/replication/Makefile (+7/-4)
database/replication/repair-restored-db.py (+41/-22)
lib/canonical/launchpad/icing/style.css (+9/-2)
lib/canonical/launchpad/javascript/code/branchlinks.js (+4/-3)
lib/canonical/launchpad/javascript/code/branchstatus.js (+2/-1)
lib/canonical/launchpad/javascript/code/branchsubscription.js (+2/-1)
lib/canonical/launchpad/javascript/code/codereview.js (+2/-1)
lib/canonical/launchpad/javascript/code/popupdiff.js (+123/-0)
lib/canonical/launchpad/webapp/configure.zcml (+0/-7)
lib/canonical/launchpad/webapp/tales.py (+0/-62)
lib/canonical/launchpad/webapp/tests/test_tales.py (+2/-135)
lib/devscripts/ec2test/builtins.py (+1/-1)
lib/lp/code/browser/branchlisting.py (+1/-8)
lib/lp/code/browser/branchmergeproposal.py (+81/-33)
lib/lp/code/browser/configure.zcml (+30/-5)
lib/lp/code/browser/diff.py (+77/-0)
lib/lp/code/browser/tests/test_branchmergeproposal.py (+55/-17)
lib/lp/code/browser/tests/test_tales.py (+163/-0)
lib/lp/code/model/branchmergeproposal.py (+11/-6)
lib/lp/code/model/directbranchcommit.py (+17/-3)
lib/lp/code/stories/branches/xx-branch-merge-proposals.txt (+43/-27)
lib/lp/code/templates/branch-index.pt (+23/-1)
lib/lp/code/templates/branch-pending-merges.pt (+1/-0)
lib/lp/code/templates/branchmergeproposal-diff.pt (+33/-0)
lib/lp/code/templates/branchmergeproposal-index.pt (+2/-28)
lib/lp/code/templates/branchmergeproposal-link-summary.pt (+2/-1)
lib/lp/code/templates/branchmergeproposal-vote-summary.pt (+52/-0)
lib/lp/code/tests/helpers.py (+42/-0)
lib/lp/code/tests/test_directbranchcommit.py (+1/-1)
lib/lp/code/windmill/tests/test_popup_diff.py (+64/-0)
lib/lp/services/browser_helpers.py (+8/-0)
lib/lp/translations/doc/translations-export-to-branch.txt (+11/-3)
lib/lp/translations/scripts/translations_to_branch.py (+13/-6)
lib/lp/translations/utilities/gettext_po_exporter.py (+15/-2)
To merge this branch: bzr merge lp:~jtv/launchpad/bug-427278
Reviewer Review Type Date Requested Status
Barry Warsaw (community) Abstain
Review via email: mp+14994@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

= Bug 427278 =

Translations exports to bzr branches can sometimes keep transactions open for a while. There's no particular reason to go that far, and it's Not Nice. We've had the script idle-killed a few times.

We've done several things that prevented a repetition:

1. We no longer export all translations for a template; now we only do ones that have changed since the last export. Makes the exports a lot faster.

2. The deadline for long transactions was extended, and obviously that's pretty effective at stopping the alarm bells from ringing. But ideal it is not.

3. The branch you see here makes the script commit more often around the likely pain points.

There's really only one catch: DirectBranchCommit checks the branch for conflicting changes. For that it needs to compare the latest revision in the branch to the last_scanned_id as stored in the database when the DirectBranchCommit was constructed. So instead of getting the last_scanned_id at the point where it's referenced, DirectBranchCommit will now retrieve and store it at construction time. This should also close off a race condition that may or may not exist in the case where database transactions are committed while a DirectBranchCommit is active.

Passing a transaction into DirectBranchCommit.commit, as I do here, is a bit weird. There are reasons for it:

 * Operations that access branch storage can take a long time, but don't need database access. That makes them excellent points to close off any ongoing transaction. The framework starts new transactions on demand, and that's going to happen only after the bzr operation completes. Until that time, the committer avoids being "idle in transaction" on the database.

 * Nowadays txn is global, so we could just "import transaction" and "transaction.commit()". But passing it as an object makes it easy to mock transactions in tests. Also, passing a boolean is slightly more error-prone. Named parameters are a big improvement--"dbc.commit(message, commit=True)"--but still relatively sensitive to mistakes.

 * Passing the transaction object to commit rather than the constructor makes it clear that you're not giving out a blanket license to commit. The DirectBranchCommit can commit only inside that one particular method call. Don't hide transaction boundaries in the program flow.

A lot of the actual diff went into nicer logging.

== Test, demo & Q/A ==

Test:
{{{
./bin/test -vv -t 'translations.*to.*branch' -t 'direct.*branch.*commit'
}}}

Create a branch on staging, and set it as the productseries branch and the translations export branch for some project. Enable translations and two-way bzr synchronization. Push a template to the branch.

Watch the project's import queue; wait for the template to be imported. Then translate a string in the template. Watch the revision log for the branch; an automatic translations commit should appear there within the hour.

No lint.

Jeroen

Revision history for this message
Barry Warsaw (barry) wrote :

Jeroen,

It looks like there's a lot more stuff in here than I'd expect from reading the cover letter. For example, why all the style and JavaScript changes? What's with the PreviewDiff changes? Something tells me that you've got a lot more changes in this merge proposal than you intend. Since I can't separate out what should be reviewed, I'm abstaining on this for now.

review: Abstain
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

Oops; forgot to MP for devel instead of the default db-devel. Filing new MP.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'database/replication/Makefile'
2--- database/replication/Makefile 2009-11-13 10:41:58 +0000
3+++ database/replication/Makefile 2009-11-18 15:05:39 +0000
4@@ -36,6 +36,8 @@
5 STAGING_CONFIG=staging # For swapping fresh db into place.
6 STAGING_DUMP=launchpad.dump # Dumpfile to build new staging from.
7 STAGING_TABLESPACE=pg_default # 'pg_default' for default
8+DOGFOOD_DBNAME=launchpad_dogfood
9+DOGFOOD_DUMP=launchpad.dump
10
11 _CONFIG=overridden-on-command-line
12 _SLAVE_TABLESPACE=pg_default
13@@ -98,12 +100,13 @@
14 psql -q -d lpmain_staging_new -f authdb_drop.sql
15 psql -q -d lpmain_staging_new -f authdb_create.sql \
16 2>&1 | grep -v _sl || true
17+ # Restore the data
18+ pg_restore --dbname=lpmain_staging_new \
19+ --no-acl --no-owner --disable-triggers --data-only \
20+ --exit-on-error ${STAGING_DUMP}
21 # Uninstall Slony-I if it is installed - a pg_dump of a DB with
22 # Slony-I installed isn't usable without this step.
23 LPCONFIG=${NEW_STAGING_CONFIG} ./repair-restored-db.py
24- pg_restore --dbname=lpmain_staging_new \
25- --no-acl --no-owner --disable-triggers --data-only \
26- --exit-on-error ${STAGING_DUMP}
27 # Setup replication
28 make _replicate LPCONFIG=${NEW_STAGING_CONFIG} LAG="0 seconds" \
29 _MASTER=lpmain_staging_new _SLAVE=lpmain_staging_slave_new \
30@@ -138,10 +141,10 @@
31 psql -q -d ${DOGFOOD_DBNAME} -f authdb_drop.sql
32 psql -q -d ${DOGFOOD_DBNAME} -f authdb_create.sql \
33 2>&1 | grep -v _sl || true
34- ./repair-restored-db.py -d ${DOGFOOD_DBNAME}
35 pg_restore --dbname=${DOGFOOD_DBNAME} --no-acl --no-owner \
36 --disable-triggers --data-only \
37 --exit-on-error ${DOGFOOD_DUMP}
38+ ./repair-restored-db.py -d ${DOGFOOD_DBNAME}
39 ../schema/upgrade.py -d ${DOGFOOD_DBNAME}
40 ../schema/fti.py -d ${DOGFOOD_DBNAME}
41 ../schema/security.py -d ${DOGFOOD_DBNAME}
42
43=== modified file 'database/replication/repair-restored-db.py'
44--- database/replication/repair-restored-db.py 2009-10-17 14:06:03 +0000
45+++ database/replication/repair-restored-db.py 2009-11-18 15:05:38 +0000
46@@ -27,7 +27,8 @@
47
48 from canonical.config import config
49 from canonical.database.postgresql import ConnectionString
50-from canonical.database.sqlbase import connect, quote
51+from canonical.database.sqlbase import (
52+ connect, quote, ISOLATION_LEVEL_AUTOCOMMIT)
53 from canonical.launchpad.scripts import db_options, logger_options, logger
54
55 import replication.helpers
56@@ -44,12 +45,23 @@
57
58 log = logger(options)
59
60- con = connect(options.dbuser)
61+ con = connect(options.dbuser, isolation=ISOLATION_LEVEL_AUTOCOMMIT)
62
63 if not replication.helpers.slony_installed(con):
64 log.info("Slony-I not installed. Nothing to do.")
65 return 0
66
67+ if not repair_with_slonik(log, options, con):
68+ repair_with_drop_schema(log, con)
69+
70+ return 0
71+
72+
73+def repair_with_slonik(log, options, con):
74+ """Attempt to uninstall Slony-I via 'UNINSTALL NODE' per best practice.
75+
76+ Returns True on success, False if unable to do so for any reason.
77+ """
78 cur = con.cursor()
79
80 # Determine the node id the database thinks it is.
81@@ -60,27 +72,19 @@
82 cur.execute(cmd)
83 node_id = cur.fetchone()[0]
84 log.debug("Node Id is %d" % node_id)
85+
86+ # Get a list of set ids in the database.
87+ cur.execute(
88+ "SELECT DISTINCT set_id FROM %s.sl_set"
89+ % replication.helpers.CLUSTER_NAMESPACE)
90+ set_ids = set(row[0] for row in cur.fetchall())
91+ log.debug("Set Ids are %s" % repr(set_ids))
92+
93 except psycopg2.InternalError:
94 # Not enough information to determine node id. Possibly
95- # this is an empty database. Just drop the _sl schema as
96- # it is 'good enough' with Slony-I 1.2 - this mechanism
97- # fails with Slony added primary keys, but we don't do that.
98- con.rollback()
99- cur = con.cursor()
100- cur.execute("DROP SCHEMA _sl CASCADE")
101- con.commit()
102- return 0
103-
104- # Get a list of set ids in the database.
105- cur.execute(
106- "SELECT DISTINCT set_id FROM %s.sl_set"
107- % replication.helpers.CLUSTER_NAMESPACE)
108- set_ids = set(row[0] for row in cur.fetchall())
109- log.debug("Set Ids are %s" % repr(set_ids))
110-
111- # Close so we don't block slonik(1)
112- del cur
113- con.close()
114+ # this is an empty database.
115+ log.debug('Broken or no Slony-I install.')
116+ return False
117
118 connection_string = ConnectionString(config.database.main_master)
119 if options.dbname:
120@@ -103,7 +107,22 @@
121 log.debug(line)
122 script = '\n'.join(script)
123
124- replication.helpers.execute_slonik(script, auto_preamble=False)
125+ return replication.helpers.execute_slonik(
126+ script, auto_preamble=False, exit_on_fail=False)
127+
128+
129+def repair_with_drop_schema(log, con):
130+ """
131+ Just drop the _sl schema as it is 'good enough' with Slony-I 1.2.
132+
133+ This mechanism fails with Slony added primary keys, but we don't
134+ do that.
135+ """
136+ log.info('Fallback mode - dropping _sl schema.')
137+ cur = con.cursor()
138+ cur.execute("DROP SCHEMA _sl CASCADE")
139+ return True
140+
141
142 if __name__ == '__main__':
143 sys.exit(main())
144
145=== modified file 'lib/canonical/launchpad/icing/style.css'
146--- lib/canonical/launchpad/icing/style.css 2009-11-10 22:09:08 +0000
147+++ lib/canonical/launchpad/icing/style.css 2009-11-18 15:05:38 +0000
148@@ -1753,10 +1753,13 @@
149 text-align: right;
150 }
151 .codereviewcomment {font: 120% monospace;}
152-.codereviewcomment .attachment, #review-diff .attachment {
153+.codereviewcomment .attachment, #review-diff .attachment,
154+div.diff .attachmentBody {
155 background-color: #f6f6f6;
156 }
157-
158+div.diff .horizontal {
159+ margin: 0;
160+}
161 body.tab-branches table.listing .section-heading {
162 color: #d18b39;
163 }
164@@ -1765,12 +1768,16 @@
165 color: #666666;
166 }
167
168+.yui-diff-overlay table.diff {
169+ font-size: 98%;
170+}
171 #proposal-summary td, #proposal-summary th { padding-bottom: 2px; }
172 table.diff {
173 white-space: pre-wrap;
174 font: 120% monospace;
175 width: 100%;
176 background-color: white;
177+ margin-bottom: 0.5em;
178 }
179 table.diff .text { width: 99%; padding-left: 0.5em; }
180 table.diff .line-no {
181
182=== modified file 'lib/canonical/launchpad/javascript/code/branchlinks.js'
183--- lib/canonical/launchpad/javascript/code/branchlinks.js 2009-08-13 13:16:04 +0000
184+++ lib/canonical/launchpad/javascript/code/branchlinks.js 2009-11-18 15:05:39 +0000
185@@ -1,4 +1,5 @@
186-/** Copyright (c) 2009, Canonical Ltd. All rights reserved.
187+/* Copyright 2009 Canonical Ltd. This software is licensed under the
188+ * GNU Affero General Public License version 3 (see the file LICENSE).
189 *
190 * Code for handling links to branches from bugs and specs.
191 *
192@@ -24,11 +25,11 @@
193 error_handler = new LP.client.ErrorHandler();
194 error_handler.clearProgressUI = function() {
195 destroy_temporary_spinner();
196- }
197+ };
198 error_handler.showError = function(error_message) {
199 alert('An unexpected error has occurred.');
200 Y.log(error_message);
201- }
202+ };
203
204 link_bug_overlay = new Y.lazr.FormOverlay({
205 headerContent: '<h2>Link to a bug</h2>',
206
207=== modified file 'lib/canonical/launchpad/javascript/code/branchstatus.js'
208--- lib/canonical/launchpad/javascript/code/branchstatus.js 2009-11-10 05:12:59 +0000
209+++ lib/canonical/launchpad/javascript/code/branchstatus.js 2009-11-18 15:05:38 +0000
210@@ -1,4 +1,5 @@
211-/** Copyright (c) 2009, Canonical Ltd. All rights reserved.
212+/* Copyright 2009 Canonical Ltd. This software is licensed under the
213+ * GNU Affero General Public License version 3 (see the file LICENSE).
214 *
215 * Code for handling the update of the branch status.
216 *
217
218=== modified file 'lib/canonical/launchpad/javascript/code/branchsubscription.js'
219--- lib/canonical/launchpad/javascript/code/branchsubscription.js 2009-08-13 13:47:24 +0000
220+++ lib/canonical/launchpad/javascript/code/branchsubscription.js 2009-11-18 15:05:39 +0000
221@@ -1,4 +1,5 @@
222-/** Copyright (c) 2009, Canonical Ltd. All rights reserved.
223+/* Copyright 2009 Canonical Ltd. This software is licensed under the
224+ * GNU Affero General Public License version 3 (see the file LICENSE).
225 *
226 * Subscription handling for branches.
227 *
228
229=== modified file 'lib/canonical/launchpad/javascript/code/codereview.js'
230--- lib/canonical/launchpad/javascript/code/codereview.js 2009-10-28 16:49:59 +0000
231+++ lib/canonical/launchpad/javascript/code/codereview.js 2009-11-18 15:05:39 +0000
232@@ -1,4 +1,5 @@
233-/** Copyright (c) 2009, Canonical Ltd. All rights reserved.
234+/* Copyright 2009 Canonical Ltd. This software is licensed under the
235+ * GNU Affero General Public License version 3 (see the file LICENSE).
236 *
237 * Library for code review javascript.
238 *
239
240=== added file 'lib/canonical/launchpad/javascript/code/popupdiff.js'
241--- lib/canonical/launchpad/javascript/code/popupdiff.js 1970-01-01 00:00:00 +0000
242+++ lib/canonical/launchpad/javascript/code/popupdiff.js 2009-11-18 15:05:38 +0000
243@@ -0,0 +1,123 @@
244+/* Copyright 2009 Canonical Ltd. This software is licensed under the
245+ * GNU Affero General Public License version 3 (see the file LICENSE).
246+ *
247+ * Code for handling the popup diffs in the pretty overlays.
248+ *
249+ * @module popupdiff
250+ * @requires node
251+ */
252+
253+YUI.add('code.branchmergeproposal.popupdiff', function(Y) {
254+
255+// The launchpad js client used.
256+var lp_client;
257+
258+/*
259+ * The DiffOverlay object inherits from the lazr-js PerttyOverlay.
260+ *
261+ * By sub-classing the DiffOverlay gets its own CSS class that is applied to
262+ * the various HTML objeccts that are created. This allows styling of the
263+ * overlay in a different way to any other PrettyOverlays.
264+ */
265+var DiffOverlay = function() {
266+ DiffOverlay.superclass.constructor.apply(this, arguments);
267+};
268+
269+Y.extend(DiffOverlay, Y.lazr.PrettyOverlay, {
270+ bindUI: function() {
271+ // call PrettyOverlay's bindUI
272+ this.constructor.superclass.bindUI.call(this);
273+ }
274+ });
275+
276+// The NAME gets appended to 'yui-' to give the class name 'yui-diff-overlay'.
277+DiffOverlay.NAME = 'diff-overlay';
278+
279+// A local page cache of the diff overlays that have been rendered.
280+// This makes subsequent views of an already loaded diff instantaneous.
281+var rendered_overlays = {};
282+
283+/*
284+ * Display the diff for the specified api_url.
285+ *
286+ * If the specified api_url has already been rendered in an overlay, show it
287+ * again. If it hasn't been loaded, show the spinner, and load the diff using
288+ * the LP API.
289+ *
290+ * If the diff fails to load, the user is taken to the librarian url just as
291+ * if Javascript was not enabled.
292+ */
293+function display_diff(node, api_url, librarian_url) {
294+
295+ // Look to see if we have rendered one already.
296+ if (rendered_overlays[api_url] !== undefined) {
297+ rendered_overlays[api_url].show();
298+ return;
299+ }
300+
301+ // Show a spinner.
302+ var html = [
303+ '<img src="/@@/spinner" alt="loading..." ',
304+ ' style="padding-left: 0.5em"/>'].join('');
305+ var spinner = Y.Node.create(html);
306+ node.appendChild(spinner);
307+
308+ // Load the diff.
309+ var config = {
310+ on: {
311+ success: function(formatted_diff) {
312+ node.removeChild(spinner);
313+ var diff_overlay = new DiffOverlay({
314+ bodyContent: Y.Node.create(formatted_diff),
315+ align: {
316+ points: [Y.WidgetPositionExt.CC,
317+ Y.WidgetPositionExt.CC]
318+ },
319+ progressbar: false
320+ });
321+ rendered_overlays[api_url] = diff_overlay;
322+ diff_overlay.render();
323+ },
324+ failure: function() {
325+ node.removeChild(spinner);
326+ // Fail over to loading the librarian link.
327+ document.location = librarian_url;
328+ }
329+ },
330+ accept: LP.client.XHTML
331+ };
332+ lp_client.get(api_url, config);
333+}
334+
335+
336+// Grab the namespace in order to be able to expose the connect method.
337+var popupdiff = Y.namespace('code.branchmergeproposal.popupdiff');
338+
339+/*
340+ * Connect the diff links to their pretty overlay function.
341+ */
342+popupdiff.connect_diff_links = function() {
343+ Y.log('connect_diff_links called');
344+ // IE doesn't like pretty overlays.
345+ if (Y.UA.ie) {
346+ return;
347+ }
348+
349+ // Setup the LP client.
350+ lp_client = new LP.client.Launchpad();
351+
352+ // var status_content = Y.get('#branch-details-status-value');
353+ var nl = Y.all('.popup-diff');
354+ nl.each(function(node, index, nodelist){
355+ var a = node.query('a');
356+ a.addClass('js-action');
357+ var librarian_url = a.getAttribute('href');
358+ var api_url = node.query('a.api-ref').getAttribute('href');
359+ a.on('click', function(e) {
360+ e.preventDefault();
361+ display_diff(a, api_url, librarian_url);
362+ });
363+ });
364+};
365+
366+ }, '0.1', {requires: ['io', 'node', 'lazr.overlay', 'lp.client']});
367
368=== modified file 'lib/canonical/launchpad/webapp/configure.zcml'
369--- lib/canonical/launchpad/webapp/configure.zcml 2009-10-20 22:13:21 +0000
370+++ lib/canonical/launchpad/webapp/configure.zcml 2009-11-18 15:05:39 +0000
371@@ -419,13 +419,6 @@
372 />
373
374 <adapter
375- for="lp.code.interfaces.diff.IPreviewDiff"
376- provides="zope.traversing.interfaces.IPathAdapter"
377- factory="canonical.launchpad.webapp.tales.PreviewDiffFormatterAPI"
378- name="fmt"
379- />
380-
381- <adapter
382 for="lp.code.interfaces.codeimport.ICodeImport"
383 provides="zope.traversing.interfaces.IPathAdapter"
384 factory="canonical.launchpad.webapp.tales.CodeImportFormatterAPI"
385
386=== modified file 'lib/canonical/launchpad/webapp/tales.py'
387--- lib/canonical/launchpad/webapp/tales.py 2009-11-14 22:40:15 +0000
388+++ lib/canonical/launchpad/webapp/tales.py 2009-11-18 15:05:39 +0000
389@@ -1407,68 +1407,6 @@
390 '%(name)s</a>: %(title)s' % self._args(view_name))
391
392
393-class PreviewDiffFormatterAPI(ObjectFormatterAPI):
394- """Formatter for preview diffs."""
395-
396- def url(self, view_name=None, rootsite=None):
397- """Use the url of the librarian file containing the diff.
398- """
399- librarian_alias = self._context.diff_text
400- if librarian_alias is None:
401- return None
402- else:
403- return librarian_alias.getURL()
404-
405- def link(self, view_name):
406- """The link to the diff should show the line count.
407-
408- Stale diffs will have a stale-diff css class.
409- Diffs with conflicts will have a conflict-diff css class.
410- Diffs with neither will have clean-diff css class.
411-
412- The title of the diff will show the number of lines added or removed
413- if available.
414-
415- :param view_name: If not None, the link will point to the page with
416- that name on this object.
417- """
418- title_words = []
419- if self._context.conflicts is not None:
420- style = 'conflicts-diff'
421- title_words.append(_('CONFLICTS'))
422- else:
423- style = 'clean-diff'
424- # Stale style overrides conflicts or clean.
425- if self._context.stale:
426- style = 'stale-diff'
427- title_words.append(_('Stale'))
428-
429- if self._context.added_lines_count:
430- title_words.append(
431- _("%s added") % self._context.added_lines_count)
432-
433- if self._context.removed_lines_count:
434- title_words.append(
435- _("%s removed") % self._context.removed_lines_count)
436-
437- args = {
438- 'line_count': _('%s lines') % self._context.diff_lines_count,
439- 'style': style,
440- 'title': ', '.join(title_words),
441- 'url': self.url(view_name),
442- }
443- # Under normal circumstances, there will be an associated file,
444- # however if the diff is empty, then there is no alias to link to.
445- if args['url'] is None:
446- return (
447- '<span title="%(title)s" class="%(style)s">'
448- '%(line_count)s</span>' % args)
449- else:
450- return (
451- '<a href="%(url)s" title="%(title)s" class="%(style)s">'
452- '<img src="/@@/download"/>&nbsp;%(line_count)s</a>' % args)
453-
454-
455 class BranchSubscriptionFormatterAPI(CustomizableFormatter):
456 """Adapter for IBranchSubscription objects to a formatted string."""
457
458
459=== modified file 'lib/canonical/launchpad/webapp/tests/test_tales.py'
460--- lib/canonical/launchpad/webapp/tests/test_tales.py 2009-10-21 17:26:37 +0000
461+++ lib/canonical/launchpad/webapp/tests/test_tales.py 2009-11-18 15:05:39 +0000
462@@ -3,20 +3,15 @@
463
464 """tales.py doctests."""
465
466-from difflib import unified_diff
467 from textwrap import dedent
468 import unittest
469
470-from storm.store import Store
471-from zope.security.proxy import removeSecurityProxy
472 from zope.testing.doctestunit import DocTestSuite
473
474-from canonical.launchpad.ftests import test_tales
475 from canonical.launchpad.testing.pages import find_tags_by_class
476 from canonical.launchpad.webapp.tales import FormattersAPI
477-from canonical.testing import (
478- DatabaseFunctionalLayer, LaunchpadFunctionalLayer)
479-from lp.testing import login, TestCase, TestCaseWithFactory
480+from canonical.testing import DatabaseFunctionalLayer
481+from lp.testing import TestCase
482
483
484 def test_requestapi():
485@@ -268,134 +263,6 @@
486 self.assertEqual(3, line_count)
487
488
489-class TestPreviewDiffFormatter(TestCaseWithFactory):
490- """Test the PreviewDiffFormatterAPI class."""
491-
492- layer = LaunchpadFunctionalLayer
493-
494- def _createPreviewDiff(self, line_count=0, added=None, removed=None,
495- conflicts=None):
496- # Login an admin to avoid the launchpad.Edit requirements.
497- login('admin@canonical.com')
498- # Create a dummy preview diff, and make sure the branches have the
499- # correct last scanned ids to ensure that the new diff is not stale.
500- bmp = self.factory.makeBranchMergeProposal()
501- if line_count:
502- content = ''.join(unified_diff('', 'random content'))
503- else:
504- content = ''
505- preview = bmp.updatePreviewDiff(
506- content, u'rev-a', u'rev-b', conflicts=conflicts)
507- bmp.source_branch.last_scanned_id = preview.source_revision_id
508- bmp.target_branch.last_scanned_id = preview.target_revision_id
509- # Update the values directly sidestepping the security.
510- naked_diff = removeSecurityProxy(preview)
511- naked_diff.diff_lines_count = line_count
512- naked_diff.added_lines_count = added
513- naked_diff.removed_lines_count = removed
514- # In order to get the canonical url of the librarian file, we need to
515- # commit.
516- # transaction.commit()
517- # Make sure that the preview diff is in the db for the test.
518- # Storm bug: 324724
519- Store.of(bmp).flush()
520- return preview
521-
522- def _createStalePreviewDiff(self, line_count=0, added=None, removed=None,
523- conflicts=None):
524- preview = self._createPreviewDiff(
525- line_count, added, removed, conflicts)
526- preview.branch_merge_proposal.source_branch.last_scanned_id = 'other'
527- return preview
528-
529- def test_creation_method(self):
530- # Just confirm that our helpers do what they say.
531- preview = self._createPreviewDiff(12, 45, 23)
532- self.assertEqual(12, preview.diff_lines_count)
533- self.assertEqual(45, preview.added_lines_count)
534- self.assertEqual(23, preview.removed_lines_count)
535- self.assertEqual(False, preview.stale)
536- self.assertEqual(True, self._createStalePreviewDiff().stale)
537-
538- def test_fmt_no_diff(self):
539- # If there is no diff, there is no link.
540- preview = self._createPreviewDiff(0)
541- self.assertEqual(
542- '<span title="" class="clean-diff">0 lines</span>',
543- test_tales('preview/fmt:link', preview=preview))
544-
545- def test_fmt_lines_no_add_or_remove(self):
546- # If there is no diff, there is no link.
547- preview = self._createPreviewDiff(10, 0, 0)
548- self.assertEqual(
549- '<a href="%s" title="" class="clean-diff">'
550- '<img src="/@@/download"/>&nbsp;10 lines</a>'
551- % preview.diff_text.getURL(),
552- test_tales('preview/fmt:link', preview=preview))
553-
554- def test_fmt_lines_some_added_no_removed(self):
555- # If there is no diff, there is no link.
556- preview = self._createPreviewDiff(10, 4, 0)
557- self.assertEqual(
558- '<a href="%s" title="4 added" class="clean-diff">'
559- '<img src="/@@/download"/>&nbsp;10 lines</a>'
560- % preview.diff_text.getURL(),
561- test_tales('preview/fmt:link', preview=preview))
562-
563- def test_fmt_lines_no_added_some_removed(self):
564- # If there is no diff, there is no link.
565- preview = self._createPreviewDiff(10, 0, 4)
566- self.assertEqual(
567- '<a href="%s" title="4 removed" class="clean-diff">'
568- '<img src="/@@/download"/>&nbsp;10 lines</a>'
569- % preview.diff_text.getURL(),
570- test_tales('preview/fmt:link', preview=preview))
571-
572- def test_fmt_lines_added_and_removed(self):
573- # If there is no diff, there is no link.
574- preview = self._createPreviewDiff(10, 6, 4)
575- self.assertEqual(
576- '<a href="%s" title="6 added, 4 removed" class="clean-diff">'
577- '<img src="/@@/download"/>&nbsp;10 lines</a>'
578- % preview.diff_text.getURL(),
579- test_tales('preview/fmt:link', preview=preview))
580-
581- def test_fmt_simple_conflicts(self):
582- # If there is no diff, there is no link.
583- preview = self._createPreviewDiff(10, 2, 3, u'conflicts')
584- self.assertEqual(
585- '<a href="%s" title="CONFLICTS, 2 added, 3 removed" '
586- 'class="conflicts-diff">'
587- '<img src="/@@/download"/>&nbsp;10 lines</a>'
588- % preview.diff_text.getURL(),
589- test_tales('preview/fmt:link', preview=preview))
590-
591- def test_fmt_stale_empty_diff(self):
592- # If there is no diff, there is no link.
593- preview = self._createStalePreviewDiff(0)
594- self.assertEqual(
595- '<span title="Stale" class="stale-diff">0 lines</span>',
596- test_tales('preview/fmt:link', preview=preview))
597-
598- def test_fmt_stale_non_empty_diff(self):
599- # If there is no diff, there is no link.
600- preview = self._createStalePreviewDiff(500, 89, 340)
601- self.assertEqual(
602- '<a href="%s" title="Stale, 89 added, 340 removed" '
603- 'class="stale-diff"><img src="/@@/download"/>&nbsp;500 lines</a>'
604- % preview.diff_text.getURL(),
605- test_tales('preview/fmt:link', preview=preview))
606-
607- def test_fmt_stale_non_empty_diff_with_conflicts(self):
608- # If there is no diff, there is no link.
609- preview = self._createStalePreviewDiff(500, 89, 340, u'conflicts')
610- self.assertEqual(
611- '<a href="%s" title="CONFLICTS, Stale, 89 added, 340 removed" '
612- 'class="stale-diff"><img src="/@@/download"/>&nbsp;500 lines</a>'
613- % preview.diff_text.getURL(),
614- test_tales('preview/fmt:link', preview=preview))
615-
616-
617 def test_suite():
618 """Return this module's doctest Suite. Unit tests are also run."""
619 suite = unittest.TestSuite()
620
621=== modified file 'lib/devscripts/ec2test/builtins.py'
622--- lib/devscripts/ec2test/builtins.py 2009-10-16 11:07:37 +0000
623+++ lib/devscripts/ec2test/builtins.py 2009-11-18 15:05:39 +0000
624@@ -557,7 +557,7 @@
625 'bzr pull -d /var/launchpad/download-cache '
626 'lp:lp-source-dependencies')
627 if public:
628- update_sourcecode_options = '--public-only'
629+ update_sourcecode_options = ' --public-only'
630 else:
631 update_sourcecode_options = ''
632 user_connection.run_with_ssh_agent(
633
634=== modified file 'lib/lp/code/browser/branchlisting.py'
635--- lib/lp/code/browser/branchlisting.py 2009-10-26 18:40:04 +0000
636+++ lib/lp/code/browser/branchlisting.py 2009-11-18 15:05:39 +0000
637@@ -91,14 +91,7 @@
638 from lp.registry.interfaces.product import IProduct
639 from lp.registry.interfaces.sourcepackage import ISourcePackageFactory
640 from lp.registry.model.sourcepackage import SourcePackage
641-
642-
643-def get_plural_text(count, singular, plural):
644- """Return 'singular' if 'count' is 1, 'plural' otherwise."""
645- if count == 1:
646- return singular
647- else:
648- return plural
649+from lp.services.browser_helpers import get_plural_text
650
651
652 class CodeVHostBreadcrumb(Breadcrumb):
653
654=== modified file 'lib/lp/code/browser/branchmergeproposal.py'
655--- lib/lp/code/browser/branchmergeproposal.py 2009-11-10 22:57:42 +0000
656+++ lib/lp/code/browser/branchmergeproposal.py 2009-11-18 15:05:39 +0000
657@@ -34,7 +34,7 @@
658
659 import simplejson
660 from zope.app.form.browser import TextAreaWidget
661-from zope.component import adapter, getUtility
662+from zope.component import adapter, adapts, getMultiAdapter, getUtility
663 from zope.event import notify as zope_notify
664 from zope.formlib import form
665 from zope.interface import Interface, implementer, implements
666@@ -60,7 +60,8 @@
667 from canonical.launchpad.webapp.breadcrumb import Breadcrumb
668 from canonical.launchpad.webapp.interfaces import IPrimaryContext
669 from canonical.launchpad.webapp.menu import NavigationMenu
670-from canonical.launchpad.webapp.tales import FormattersAPI
671+from canonical.launchpad.webapp.tales import (
672+ DateTimeFormatterAPI, FormattersAPI)
673 from canonical.widgets.lazrjs import (
674 TextAreaEditorWidget, vocabulary_to_choice_edit_items)
675
676@@ -74,6 +75,7 @@
677 from lp.code.interfaces.codereviewcomment import ICodeReviewComment
678 from lp.code.interfaces.codereviewvote import (
679 ICodeReviewVoteReference)
680+from lp.code.interfaces.diff import IPreviewDiff
681 from lp.registry.interfaces.person import IPersonSet
682 from lp.services.comments.interfaces.conversation import IConversation
683
684@@ -138,6 +140,23 @@
685 }
686 return friendly_texts[self.context.queue_status]
687
688+ @property
689+ def status_title(self):
690+ """The title for the status text.
691+
692+ Only set if the status is approved or rejected.
693+ """
694+ result = ''
695+ if self.context.queue_status in (
696+ BranchMergeProposalStatus.CODE_APPROVED,
697+ BranchMergeProposalStatus.REJECTED
698+ ):
699+ formatter = DateTimeFormatterAPI(self.context.date_reviewed)
700+ result = '%s %s' % (
701+ self.context.reviewer.displayname,
702+ formatter.displaydate())
703+ return result
704+
705
706 class BranchMergeProposalMenuMixin:
707 """Mixin class for merge proposal menus."""
708@@ -427,10 +446,44 @@
709 return SimpleVocabulary(terms)
710
711
712+class DiffRenderingMixin:
713+ """A mixin class for handling diff text."""
714+
715+ @cachedproperty
716+ def preview_diff_text(self):
717+ """Return a (hopefully) intelligently encoded review diff."""
718+ preview_diff = self.preview_diff
719+ if preview_diff is None:
720+ return None
721+ try:
722+ diff = preview_diff.text.decode('utf-8')
723+ except UnicodeDecodeError:
724+ diff = preview_diff.text.decode('windows-1252', 'replace')
725+ # Strip off the trailing carriage returns.
726+ return diff.rstrip('\n')
727+
728+ @cachedproperty
729+ def diff_oversized(self):
730+ """Return True if the review_diff is over the configured size limit.
731+
732+ The diff can be over the limit in two ways. If the diff is oversized
733+ in bytes it will be cut off at the Diff.text method. If the number of
734+ lines is over the max_format_lines, then it is cut off at the fmt:diff
735+ processing.
736+ """
737+ review_diff = self.preview_diff
738+ if review_diff is None:
739+ return False
740+ if review_diff.oversized:
741+ return True
742+ diff_text = self.preview_diff_text
743+ return diff_text.count('\n') >= config.diff.max_format_lines
744+
745
746 class BranchMergeProposalView(LaunchpadFormView, UnmergedRevisionsMixin,
747 BranchMergeProposalRevisionIdMixin,
748- BranchMergeProposalStatusMixin):
749+ BranchMergeProposalStatusMixin,
750+ DiffRenderingMixin):
751 """A basic view used for the index page."""
752
753 implements(IBranchMergeProposalActionMenu)
754@@ -496,36 +549,6 @@
755 return self.context.review_diff.diff
756 return None
757
758- @cachedproperty
759- def preview_diff_text(self):
760- """Return a (hopefully) intelligently encoded review diff."""
761- preview_diff = self.preview_diff
762- if preview_diff is None:
763- return None
764- try:
765- diff = preview_diff.text.decode('utf-8')
766- except UnicodeDecodeError:
767- diff = preview_diff.text.decode('windows-1252', 'replace')
768- # Strip off the trailing carriage returns.
769- return diff.rstrip('\n')
770-
771- @cachedproperty
772- def review_diff_oversized(self):
773- """Return True if the review_diff is over the configured size limit.
774-
775- The diff can be over the limit in two ways. If the diff is oversized
776- in bytes it will be cut off at the Diff.text method. If the number of
777- lines is over the max_format_lines, then it is cut off at the fmt:diff
778- processing.
779- """
780- review_diff = self.preview_diff
781- if review_diff is None:
782- return False
783- if review_diff.oversized:
784- return True
785- diff_text = self.preview_diff_text
786- return diff_text.count('\n') >= config.diff.max_format_lines
787-
788 @property
789 def has_bug_or_spec(self):
790 """Return whether or not the merge proposal has a linked bug or spec.
791@@ -1290,3 +1313,28 @@
792 html = formatter(nomail).text_to_html()
793 return html.encode('utf-8')
794 return renderer
795+
796+
797+class FormatPreviewDiffView(LaunchpadView, DiffRenderingMixin):
798+ """A simple view to render a diff formatted nicely."""
799+
800+ __used_for__ = IPreviewDiff
801+
802+ @property
803+ def preview_diff(self):
804+ return self.context
805+
806+
807+class PreviewDiffHTMLRepresentation:
808+ adapts(IPreviewDiff, IWebServiceClientRequest)
809+ implements(Interface)
810+
811+ def __init__(self, diff, request):
812+ self.diff = diff
813+ self.request = request
814+
815+ def __call__(self):
816+ """Render `BugBranch` as XHTML using the webservice."""
817+ diff_view = getMultiAdapter(
818+ (self.diff, self.request), name="+diff")
819+ return diff_view()
820
821=== modified file 'lib/lp/code/browser/configure.zcml'
822--- lib/lp/code/browser/configure.zcml 2009-11-13 20:12:17 +0000
823+++ lib/lp/code/browser/configure.zcml 2009-11-18 15:05:38 +0000
824@@ -181,16 +181,24 @@
825 name="+index"
826 template="../templates/branchmergeproposal-index.pt"/>
827 <browser:page
828+ name="++diff"
829+ template="../templates/branchmergeproposal-diff.pt"/>
830+ <browser:page
831 name="+pagelet-summary"
832 template="../templates/branchmergeproposal-pagelet-summary.pt"/>
833 </browser:pages>
834- <browser:page
835- name="+votes"
836+ <browser:pages
837 for="lp.code.interfaces.branchmergeproposal.IBranchMergeProposal"
838 class="lp.code.browser.branchmergeproposal.BranchMergeProposalVoteView"
839 facet="branches"
840- permission="launchpad.View"
841- template="../templates/branchmergeproposal-votes.pt"/>
842+ permission="launchpad.View">
843+ <browser:page
844+ name="+votes"
845+ template="../templates/branchmergeproposal-votes.pt"/>
846+ <browser:page
847+ name="+vote-summary"
848+ template="../templates/branchmergeproposal-vote-summary.pt"/>
849+ </browser:pages>
850 <browser:pages
851 for="lp.code.interfaces.branchmergeproposal.IBranchMergeProposal"
852 class="lp.code.browser.branchmergeproposal.BranchMergeProposalEditView"
853@@ -276,7 +284,7 @@
854 class="lp.code.browser.branchmergeproposal.BranchMergeCandidateView"
855 facet="branches"
856 permission="zope.Public"
857- template="../templates/branch-merge-link-summary.pt"/>
858+ template="../templates/branchmergeproposal-link-summary.pt"/>
859 <browser:page
860 name="+pagelet-subscribers"
861 for="lp.code.interfaces.branchmergeproposal.IBranchMergeProposal"
862@@ -898,6 +906,13 @@
863 attribute_to_parent="branch_merge_proposal"
864 rootsite="code" />
865
866+ <browser:page
867+ name="+diff"
868+ for="lp.code.interfaces.diff.IPreviewDiff"
869+ class="lp.code.browser.branchmergeproposal.FormatPreviewDiffView"
870+ permission="launchpad.AnyPerson"
871+ template="../templates/branchmergeproposal-diff.pt"/>
872+
873 <browser:menus
874 module="lp.code.browser.branchlisting"
875 classes="PersonProductBranchesMenu"/>
876@@ -991,5 +1006,15 @@
877 for="lp.code.interfaces.branchmergeproposal.IBranchMergeProposal"
878 factory="lp.code.browser.branchmergeproposal.BranchMergeProposalBreadcrumb"
879 permission="zope.Public"/>
880+ <adapter
881+ factory="lp.code.browser.branchmergeproposal.PreviewDiffHTMLRepresentation"
882+ name="lazr.restful.EntryResource"/>
883+
884+ <adapter
885+ for="lp.code.interfaces.diff.IPreviewDiff"
886+ provides="zope.traversing.interfaces.IPathAdapter"
887+ factory="lp.code.browser.diff.PreviewDiffFormatterAPI"
888+ name="fmt"
889+ />
890
891 </configure>
892
893=== added file 'lib/lp/code/browser/diff.py'
894--- lib/lp/code/browser/diff.py 1970-01-01 00:00:00 +0000
895+++ lib/lp/code/browser/diff.py 2009-11-18 15:05:38 +0000
896@@ -0,0 +1,77 @@
897+# Copyright 2009 Canonical Ltd. This software is licensed under the
898+# GNU Affero General Public License version 3 (see the file LICENSE).
899+
900+"""Display classes relating to diff objects of one sort or another."""
901+
902+__metaclass__ = type
903+__all__ = [
904+ 'PreviewDiffFormatterAPI',
905+ ]
906+
907+
908+from canonical.launchpad import _
909+from canonical.launchpad.webapp.tales import ObjectFormatterAPI
910+from lp.services.browser_helpers import get_plural_text
911+
912+
913+class PreviewDiffFormatterAPI(ObjectFormatterAPI):
914+ """Formatter for preview diffs."""
915+
916+ def url(self, view_name=None, rootsite=None):
917+ """Use the url of the librarian file containing the diff.
918+ """
919+ librarian_alias = self._context.diff_text
920+ if librarian_alias is None:
921+ return None
922+ else:
923+ return librarian_alias.getURL()
924+
925+ def link(self, view_name):
926+ """The link to the diff should show the line count.
927+
928+ Stale diffs will have a stale-diff css class.
929+ Diffs with conflicts will have a conflict-diff css class.
930+ Diffs with neither will have clean-diff css class.
931+
932+ The title of the diff will show the number of lines added or removed
933+ if available.
934+
935+ :param view_name: If not None, the link will point to the page with
936+ that name on this object.
937+ """
938+ diff = self._context
939+ conflict_text = ''
940+ if diff.conflicts is not None:
941+ conflict_text = _(' (has conflicts)')
942+
943+ count_text = ''
944+ added = diff.added_lines_count
945+ removed = diff.removed_lines_count
946+ if (added is not None and removed is not None):
947+ count_text = ' (+%d/-%d)' % (added, removed)
948+
949+ file_text = ''
950+ diffstat = diff.diffstat
951+ if diffstat is not None:
952+ file_count = len(diffstat)
953+ file_text = get_plural_text(
954+ file_count, _(' %d file modified'), _(' %d files modified'))
955+ file_text = file_text % file_count
956+
957+ args = {
958+ 'line_count': _('%s lines') % diff.diff_lines_count,
959+ 'file_text': file_text,
960+ 'conflict_text': conflict_text,
961+ 'count_text': count_text,
962+ 'url': self.url(view_name),
963+ }
964+ # Under normal circumstances, there will be an associated file,
965+ # however if the diff is empty, then there is no alias to link to.
966+ if args['url'] is None:
967+ return (
968+ '<span class="empty-diff">'
969+ '%(line_count)s</span>' % args)
970+ else:
971+ return (
972+ '<a href="%(url)s" class="diff-link">'
973+ '%(line_count)s%(count_text)s%(file_text)s%(conflict_text)s</a>' % args)
974
975=== modified file 'lib/lp/code/browser/tests/test_branchmergeproposal.py'
976--- lib/lp/code/browser/tests/test_branchmergeproposal.py 2009-11-01 23:13:09 +0000
977+++ lib/lp/code/browser/tests/test_branchmergeproposal.py 2009-11-18 15:05:38 +0000
978@@ -7,10 +7,11 @@
979
980 __metaclass__ = type
981
982-from datetime import timedelta
983+from datetime import datetime, timedelta
984 from difflib import unified_diff
985 import unittest
986
987+import pytz
988 import transaction
989 from zope.component import getMultiAdapter
990 from zope.security.interfaces import Unauthorized
991@@ -66,6 +67,7 @@
992 link = menu.add_comment()
993 self.assertTrue(menu.add_comment().enabled)
994
995+
996 class TestDecoratedCodeReviewVoteReference(TestCaseWithFactory):
997
998 layer = DatabaseFunctionalLayer
999@@ -459,13 +461,6 @@
1000 self.bmp = self.factory.makeBranchMergeProposal(registrant=self.user)
1001 login_person(self.user)
1002
1003- def _createView(self):
1004- # Construct the view and initialize it.
1005- view = BranchMergeProposalView(
1006- self.bmp, LaunchpadTestRequest())
1007- view.initialize()
1008- return view
1009-
1010 def makeTeamReview(self):
1011 owner = self.bmp.source_branch.owner
1012 review_team = self.factory.makeTeam()
1013@@ -477,7 +472,7 @@
1014 albert = self.factory.makePerson()
1015 albert.join(review.reviewer)
1016 login_person(albert)
1017- view = self._createView()
1018+ view = create_initialized_view(self.bmp, '+index')
1019 view.claim_action.success({'review_id': review.id})
1020 self.assertEqual(albert, review.reviewer)
1021
1022@@ -486,13 +481,13 @@
1023 review = self.makeTeamReview()
1024 albert = self.factory.makePerson()
1025 login_person(albert)
1026- view = self._createView()
1027+ view = create_initialized_view(self.bmp, '+index')
1028 self.assertRaises(Unauthorized, view.claim_action.success,
1029 {'review_id': review.id})
1030
1031 def test_preview_diff_text_with_no_diff(self):
1032 """review_diff should be None when there is no context.review_diff."""
1033- view = self._createView()
1034+ view = create_initialized_view(self.bmp, '+index')
1035 self.assertIs(None, view.preview_diff_text)
1036
1037 def test_review_diff_utf8(self):
1038@@ -502,8 +497,9 @@
1039 diff = StaticDiff.acquireFromText('x', 'y', diff_bytes)
1040 transaction.commit()
1041 self.bmp.review_diff = diff
1042+ view = create_initialized_view(self.bmp, '+index')
1043 self.assertEqual(diff_bytes.decode('utf-8'),
1044- self._createView().preview_diff_text)
1045+ view.preview_diff_text)
1046
1047 def test_review_diff_all_chars(self):
1048 """review_diff should work on diffs containing all possible bytes."""
1049@@ -512,8 +508,9 @@
1050 diff = StaticDiff.acquireFromText('x', 'y', diff_bytes)
1051 transaction.commit()
1052 self.bmp.review_diff = diff
1053+ view = create_initialized_view(self.bmp, '+index')
1054 self.assertEqual(diff_bytes.decode('windows-1252', 'replace'),
1055- self._createView().preview_diff_text)
1056+ view.preview_diff_text)
1057
1058 def addReviewDiff(self):
1059 review_diff_bytes = ''.join(unified_diff('', 'review'))
1060@@ -532,27 +529,31 @@
1061 def test_preview_diff_prefers_preview_diff(self):
1062 """The preview will be used for BMP with both a review and preview."""
1063 preview_diff = self.addBothDiffs()
1064- self.assertEqual(preview_diff, self._createView().preview_diff)
1065+ view = create_initialized_view(self.bmp, '+index')
1066+ self.assertEqual(preview_diff, view.preview_diff)
1067
1068 def test_preview_diff_uses_review_diff(self):
1069 """The review diff will be used if there is no preview."""
1070 review_diff = self.addReviewDiff()
1071+ view = create_initialized_view(self.bmp, '+index')
1072 self.assertEqual(review_diff.diff,
1073- self._createView().preview_diff)
1074+ view.preview_diff)
1075
1076 def test_review_diff_text_prefers_preview_diff(self):
1077 """The preview will be used for BMP with both a review and preview."""
1078 preview_diff = self.addBothDiffs()
1079 transaction.commit()
1080+ view = create_initialized_view(self.bmp, '+index')
1081 self.assertEqual(
1082- preview_diff.text, self._createView().preview_diff_text)
1083+ preview_diff.text, view.preview_diff_text)
1084
1085 def test_linked_bugs_excludes_mutual_bugs(self):
1086 """List bugs that are linked to the source only."""
1087 bug = self.factory.makeBug()
1088 self.bmp.source_branch.linkBug(bug, self.bmp.registrant)
1089 self.bmp.target_branch.linkBug(bug, self.bmp.registrant)
1090- self.assertEqual([], self._createView().linked_bugs)
1091+ view = create_initialized_view(self.bmp, '+index')
1092+ self.assertEqual([], view.linked_bugs)
1093
1094
1095 class TestBranchMergeProposalChangeStatusOptions(TestCaseWithFactory):
1096@@ -692,5 +693,42 @@
1097 self.assertEqual(u'\u2615', diff_attachment.diff_text)
1098
1099
1100+class TestBranchMergeCandidateView(TestCaseWithFactory):
1101+ """Test the status title for the view."""
1102+
1103+ layer = DatabaseFunctionalLayer
1104+
1105+ def test_needs_review_title(self):
1106+ # No title is set for a proposal needing review.
1107+ bmp = self.factory.makeBranchMergeProposal(
1108+ set_state=BranchMergeProposalStatus.NEEDS_REVIEW)
1109+ view = create_initialized_view(bmp, '+link-summary')
1110+ self.assertEqual('', view.status_title)
1111+
1112+ def test_approved_shows_reviewer(self):
1113+ # If the proposal is approved, the approver is shown in the title
1114+ # along with when they approved it.
1115+ bmp = self.factory.makeBranchMergeProposal()
1116+ owner = bmp.target_branch.owner
1117+ login_person(bmp.target_branch.owner)
1118+ owner.displayname = 'Eric'
1119+ bmp.approveBranch(owner, 'some-rev', datetime(
1120+ year=2008, month=9, day=10, tzinfo=pytz.UTC))
1121+ view = create_initialized_view(bmp, '+link-summary')
1122+ self.assertEqual('Eric on 2008-09-10', view.status_title)
1123+
1124+ def test_rejected_shows_reviewer(self):
1125+ # If the proposal is rejected, the approver is shown in the title
1126+ # along with when they approved it.
1127+ bmp = self.factory.makeBranchMergeProposal()
1128+ owner = bmp.target_branch.owner
1129+ login_person(bmp.target_branch.owner)
1130+ owner.displayname = 'Eric'
1131+ bmp.rejectBranch(owner, 'some-rev', datetime(
1132+ year=2008, month=9, day=10, tzinfo=pytz.UTC))
1133+ view = create_initialized_view(bmp, '+link-summary')
1134+ self.assertEqual('Eric on 2008-09-10', view.status_title)
1135+
1136+
1137 def test_suite():
1138 return unittest.TestLoader().loadTestsFromName(__name__)
1139
1140=== added file 'lib/lp/code/browser/tests/test_tales.py'
1141--- lib/lp/code/browser/tests/test_tales.py 1970-01-01 00:00:00 +0000
1142+++ lib/lp/code/browser/tests/test_tales.py 2009-11-18 15:05:39 +0000
1143@@ -0,0 +1,163 @@
1144+# Copyright 2009 Canonical Ltd. This software is licensed under the
1145+# GNU Affero General Public License version 3 (see the file LICENSE).
1146+
1147+"""Tests for the tales formatters."""
1148+
1149+__metaclass__ = type
1150+
1151+from difflib import unified_diff
1152+import unittest
1153+
1154+from storm.store import Store
1155+from zope.security.proxy import removeSecurityProxy
1156+
1157+from lp.testing import login, TestCaseWithFactory, test_tales
1158+from canonical.testing import LaunchpadFunctionalLayer
1159+
1160+
1161+class TestPreviewDiffFormatter(TestCaseWithFactory):
1162+ """Test the PreviewDiffFormatterAPI class."""
1163+
1164+ layer = LaunchpadFunctionalLayer
1165+
1166+ def _createPreviewDiff(self, line_count=0, added=None, removed=None,
1167+ conflicts=None, diffstat=None):
1168+ # Login an admin to avoid the launchpad.Edit requirements.
1169+ login('admin@canonical.com')
1170+ # Create a dummy preview diff, and make sure the branches have the
1171+ # correct last scanned ids to ensure that the new diff is not stale.
1172+ bmp = self.factory.makeBranchMergeProposal()
1173+ if line_count:
1174+ content = ''.join(unified_diff('', 'random content'))
1175+ else:
1176+ content = ''
1177+ preview = bmp.updatePreviewDiff(
1178+ content, u'rev-a', u'rev-b', conflicts=conflicts)
1179+ bmp.source_branch.last_scanned_id = preview.source_revision_id
1180+ bmp.target_branch.last_scanned_id = preview.target_revision_id
1181+ # Update the values directly sidestepping the security.
1182+ naked_diff = removeSecurityProxy(preview)
1183+ naked_diff.diff_lines_count = line_count
1184+ naked_diff.added_lines_count = added
1185+ naked_diff.removed_lines_count = removed
1186+ naked_diff.diffstat = diffstat
1187+ # In order to get the canonical url of the librarian file, we need to
1188+ # commit.
1189+ # transaction.commit()
1190+ # Make sure that the preview diff is in the db for the test.
1191+ # Storm bug: 324724
1192+ Store.of(bmp).flush()
1193+ return preview
1194+
1195+ def _createStalePreviewDiff(self, line_count=0, added=None, removed=None,
1196+ conflicts=None, diffstat=None):
1197+ preview = self._createPreviewDiff(
1198+ line_count, added, removed, conflicts, diffstat)
1199+ preview.branch_merge_proposal.source_branch.last_scanned_id = 'other'
1200+ return preview
1201+
1202+ def test_creation_method(self):
1203+ # Just confirm that our helpers do what they say.
1204+ preview = self._createPreviewDiff(
1205+ 12, 45, 23, u'conflicts', {'filename': (3,2)})
1206+ self.assertEqual(12, preview.diff_lines_count)
1207+ self.assertEqual(45, preview.added_lines_count)
1208+ self.assertEqual(23, preview.removed_lines_count)
1209+ self.assertEqual(False, preview.stale)
1210+ self.assertEqual(True, self._createStalePreviewDiff().stale)
1211+ self.assertEqual(u'conflicts', preview.conflicts)
1212+ self.assertEqual({'filename': (3,2)}, preview.diffstat)
1213+
1214+ def test_fmt_no_diff(self):
1215+ # If there is no diff, there is no link.
1216+ preview = self._createPreviewDiff(0)
1217+ self.assertEqual(
1218+ '<span class="empty-diff">0 lines</span>',
1219+ test_tales('preview/fmt:link', preview=preview))
1220+
1221+ def test_fmt_lines_no_add_or_remove(self):
1222+ # If the lines added and removed are None, they are now shown.
1223+ preview = self._createPreviewDiff(10, added=None, removed=None)
1224+ self.assertEqual(
1225+ '<a href="%s" class="diff-link">'
1226+ '10 lines</a>'
1227+ % preview.diff_text.getURL(),
1228+ test_tales('preview/fmt:link', preview=preview))
1229+
1230+ def test_fmt_lines_some_added_no_removed(self):
1231+ # If the added and removed values are not None, they are shown.
1232+ preview = self._createPreviewDiff(10, added=4, removed=0)
1233+ self.assertEqual(
1234+ '<a href="%s" class="diff-link">'
1235+ '10 lines (+4/-0)</a>'
1236+ % preview.diff_text.getURL(),
1237+ test_tales('preview/fmt:link', preview=preview))
1238+
1239+ def test_fmt_lines_files_modified(self):
1240+ # If the diffstat has been set, the number of entries in the dict
1241+ # defines the number of files modified.
1242+ preview = self._createPreviewDiff(
1243+ 10, added=4, removed=0, diffstat={
1244+ 'file1': (1, 0),
1245+ 'file2': (3, 0)})
1246+ self.assertEqual(
1247+ '<a href="%s" class="diff-link">'
1248+ '10 lines (+4/-0) 2 files modified</a>'
1249+ % preview.diff_text.getURL(),
1250+ test_tales('preview/fmt:link', preview=preview))
1251+
1252+ def test_fmt_lines_one_file_modified(self):
1253+ # If only one file has been modified, a singular value is used.
1254+ preview = self._createPreviewDiff(
1255+ 10, added=4, removed=0, diffstat={
1256+ 'file': (3, 0)})
1257+ self.assertEqual(
1258+ '<a href="%s" class="diff-link">'
1259+ '10 lines (+4/-0) 1 file modified</a>'
1260+ % preview.diff_text.getURL(),
1261+ test_tales('preview/fmt:link', preview=preview))
1262+
1263+ def test_fmt_simple_conflicts(self):
1264+ # Conflicts are indicated using text in the link.
1265+ preview = self._createPreviewDiff(10, 2, 3, u'conflicts')
1266+ self.assertEqual(
1267+ '<a href="%s" class="diff-link">'
1268+ '10 lines (+2/-3) (has conflicts)</a>'
1269+ % preview.diff_text.getURL(),
1270+ test_tales('preview/fmt:link', preview=preview))
1271+
1272+ def test_fmt_stale_empty_diff(self):
1273+ # If there is no diff, there is no link.
1274+ preview = self._createStalePreviewDiff(0)
1275+ self.assertEqual(
1276+ '<span class="empty-diff">0 lines</span>',
1277+ test_tales('preview/fmt:link', preview=preview))
1278+
1279+ def test_fmt_stale_non_empty_diff(self):
1280+ # If there is no diff, there is no link.
1281+ diffstat = dict(
1282+ (self.factory.getUniqueString(), (2,3)) for x in range(23))
1283+ preview = self._createStalePreviewDiff(
1284+ 500, 89, 340, diffstat=diffstat)
1285+ self.assertEqual(
1286+ '<a href="%s" class="diff-link">'
1287+ '500 lines (+89/-340) 23 files modified</a>'
1288+ % preview.diff_text.getURL(),
1289+ test_tales('preview/fmt:link', preview=preview))
1290+
1291+ def test_fmt_stale_non_empty_diff_with_conflicts(self):
1292+ # If there is no diff, there is no link.
1293+ diffstat = dict(
1294+ (self.factory.getUniqueString(), (2,3)) for x in range(23))
1295+ preview = self._createStalePreviewDiff(
1296+ 500, 89, 340, u'conflicts', diffstat=diffstat)
1297+ self.assertEqual(
1298+ '<a href="%s" class="diff-link">'
1299+ '500 lines (+89/-340) 23 files modified (has conflicts)</a>'
1300+ % preview.diff_text.getURL(),
1301+ test_tales('preview/fmt:link', preview=preview))
1302+
1303+
1304+def test_suite():
1305+ return unittest.TestLoader().loadTestsFromName(__name__)
1306+
1307
1308=== modified file 'lib/lp/code/model/branchmergeproposal.py'
1309--- lib/lp/code/model/branchmergeproposal.py 2009-10-05 14:17:48 +0000
1310+++ lib/lp/code/model/branchmergeproposal.py 2009-11-18 15:05:38 +0000
1311@@ -361,7 +361,8 @@
1312 # or superseded, then it is valid to be merged.
1313 return (self.queue_status not in FINAL_STATES)
1314
1315- def _reviewProposal(self, reviewer, next_state, revision_id):
1316+ def _reviewProposal(self, reviewer, next_state, revision_id,
1317+ _date_reviewed=None):
1318 """Set the proposal to one of the two review statuses."""
1319 # Check the reviewer can review the code for the target branch.
1320 old_state = self.queue_status
1321@@ -371,21 +372,25 @@
1322 self._transitionToState(next_state, reviewer)
1323 # Record the reviewer
1324 self.reviewer = reviewer
1325- self.date_reviewed = UTC_NOW
1326+ if _date_reviewed is None:
1327+ _date_reviewed = UTC_NOW
1328+ self.date_reviewed = _date_reviewed
1329 # Record the reviewed revision id
1330 self.reviewed_revision_id = revision_id
1331 notify(BranchMergeProposalStatusChangeEvent(
1332 self, reviewer, old_state, next_state))
1333
1334- def approveBranch(self, reviewer, revision_id):
1335+ def approveBranch(self, reviewer, revision_id, _date_reviewed=None):
1336 """See `IBranchMergeProposal`."""
1337 self._reviewProposal(
1338- reviewer, BranchMergeProposalStatus.CODE_APPROVED, revision_id)
1339+ reviewer, BranchMergeProposalStatus.CODE_APPROVED, revision_id,
1340+ _date_reviewed)
1341
1342- def rejectBranch(self, reviewer, revision_id):
1343+ def rejectBranch(self, reviewer, revision_id, _date_reviewed=None):
1344 """See `IBranchMergeProposal`."""
1345 self._reviewProposal(
1346- reviewer, BranchMergeProposalStatus.REJECTED, revision_id)
1347+ reviewer, BranchMergeProposalStatus.REJECTED, revision_id,
1348+ _date_reviewed)
1349
1350 def enqueue(self, queuer, revision_id):
1351 """See `IBranchMergeProposal`."""
1352
1353=== modified file 'lib/lp/code/model/directbranchcommit.py'
1354--- lib/lp/code/model/directbranchcommit.py 2009-09-09 20:03:19 +0000
1355+++ lib/lp/code/model/directbranchcommit.py 2009-11-18 15:05:38 +0000
1356@@ -74,6 +74,8 @@
1357 self.db_branch = db_branch
1358 self.to_mirror = to_mirror
1359
1360+ self.last_scanned_id = self.db_branch.last_scanned_id
1361+
1362 if committer is None:
1363 committer = db_branch.owner
1364 self.committer = committer
1365@@ -172,12 +174,18 @@
1366 assert self.is_locked, "Getting revision on un-locked branch."
1367 last_revision = None
1368 last_revision = self.bzrbranch.last_revision()
1369- if last_revision != self.db_branch.last_scanned_id:
1370+ if last_revision != self.last_scanned_id:
1371 raise ConcurrentUpdateError(
1372 "Branch has been changed. Not committing.")
1373
1374- def commit(self, commit_message):
1375- """Commit to branch."""
1376+ def commit(self, commit_message, txn=None):
1377+ """Commit to branch.
1378+
1379+ :param commit_message: Message for branch's commit log.
1380+ :param txn: Transaction to commit. Can be helpful in avoiding
1381+ long idle times in database transactions. May be committed
1382+ more than once.
1383+ """
1384 assert self.is_open, "Committing closed DirectBranchCommit."
1385 assert self.is_locked, "Not locked at commit time."
1386
1387@@ -185,6 +193,9 @@
1388 try:
1389 self._checkForRace()
1390
1391+ if txn:
1392+ txn.commit()
1393+
1394 rev_id = self.revision_tree.get_revision_id()
1395 if rev_id == NULL_REVISION:
1396 if list(self.transform_preview.iter_changes()) == []:
1397@@ -193,6 +204,9 @@
1398 self.bzrbranch, commit_message)
1399 IMasterObject(self.db_branch).requestMirror()
1400
1401+ if txn:
1402+ txn.commit()
1403+
1404 finally:
1405 self.unlock()
1406 self.is_open = False
1407
1408=== modified file 'lib/lp/code/stories/branches/xx-branch-merge-proposals.txt'
1409--- lib/lp/code/stories/branches/xx-branch-merge-proposals.txt 2009-10-28 19:31:47 +0000
1410+++ lib/lp/code/stories/branches/xx-branch-merge-proposals.txt 2009-11-18 15:05:39 +0000
1411@@ -29,6 +29,8 @@
1412 >>> _unused = branch.subscribe(subscriber,
1413 ... BranchSubscriptionNotificationLevel.NOEMAIL, None,
1414 ... CodeReviewNotificationLevel.FULL)
1415+ >>> from lp.code.tests.helpers import make_erics_fooix_project
1416+ >>> locals().update(make_erics_fooix_project(factory))
1417 >>> logout()
1418
1419 Any logged in user can register a merge proposal. Registering
1420@@ -68,7 +70,8 @@
1421
1422 Registering the merge proposal takes the user to the new merge proposal.
1423
1424- >>> print nopriv_browser.url
1425+ >>> klingon_proposal = nopriv_browser.url
1426+ >>> print klingon_proposal
1427 http://code.launchpad.dev/~name12/gnome-terminal/klingon/+merge/...
1428
1429 The summary reflects the selected target and prerequisite.
1430@@ -97,9 +100,6 @@
1431 ... 'Add more <b>mojo</b>')
1432 >>> nopriv_browser.getControl('Update').click()
1433
1434- >>> print nopriv_browser.url
1435- http://code.launchpad.dev/~name12/gnome-terminal/klingon/+merge/1
1436-
1437 >>> print_tag_with_id(nopriv_browser.contents, 'edit-description')
1438 Commit Message
1439 Add more &lt;b&gt;mojo&lt;/b&gt;
1440@@ -113,7 +113,6 @@
1441 for the source_branch.
1442
1443 >>> login('foo.bar@canonical.com')
1444- >>> eric = factory.makePerson(email="eric@example.com", password="test")
1445 >>> bmp = factory.makeBranchMergeProposal(registrant=eric)
1446 >>> bmp_url = canonical_url(bmp)
1447 >>> branch_url = canonical_url(bmp.source_branch)
1448@@ -128,14 +127,13 @@
1449
1450 >>> sample_browser = setupBrowser(auth="Basic test@canonical.com:test")
1451
1452+
1453 Requesting reviews
1454 ------------------
1455
1456 You can request a review of a merge proposal.
1457
1458- >>> sample_browser.open(
1459- ... 'http://code.launchpad.dev/~name12/gnome-terminal/klingon/+merge/1'
1460- ... )
1461+ >>> sample_browser.open(klingon_proposal)
1462 >>> sample_browser.getLink('Request a review').click()
1463 >>> sample_browser.getControl('Reviewer').value = 'mark'
1464 >>> sample_browser.getControl('Review type').value = 'first'
1465@@ -169,7 +167,7 @@
1466 Reviewer Review Type Date Requested Status
1467 Sample Person Pending [Review]
1468 Mark Shuttleworth second ... ago Pending
1469- Review via email: mp+1@code.launchpad.dev
1470+ Review via email: mp+...@code.launchpad.dev
1471 Request another review
1472
1473
1474@@ -178,16 +176,14 @@
1475
1476 People not logged in cannot perform reviews.
1477
1478- >>> anon_browser.open('http://code.launchpad.dev/~name12/gnome-terminal'
1479- ... '/klingon/+merge/1')
1480+ >>> anon_browser.open(klingon_proposal)
1481 >>> link = anon_browser.getLink('[Review]')
1482 Traceback (most recent call last):
1483 LinkNotFoundError
1484
1485 People who are logged in can perform reviews.
1486
1487- >>> nopriv_browser.open('http://code.launchpad.dev/~name12/gnome-terminal'
1488- ... '/klingon/+merge/1')
1489+ >>> nopriv_browser.open(klingon_proposal)
1490 >>> nopriv_browser.getLink('Add a review or comment').click()
1491 >>> nopriv_browser.getControl(name='field.comment').value = "Don't like it"
1492 >>> nopriv_browser.getControl(name='field.vote').getControl(
1493@@ -201,8 +197,7 @@
1494
1495 People can claim reviews for teams of which they are a member.
1496
1497- >>> sample_browser.open('http://code.launchpad.dev/~name12/gnome-terminal'
1498- ... '/klingon/+merge/1')
1499+ >>> sample_browser.open(klingon_proposal)
1500 >>> sample_browser.getLink('Request another review').click()
1501 >>> sample_browser.getControl('Reviewer').value = 'hwdb-team'
1502 >>> sample_browser.getControl('Review type').value = 'claimable'
1503@@ -213,8 +208,7 @@
1504 Reviewer Review Type Date Requested Status...
1505 HWDB Team claimable ... ago Pending ...
1506 >>> foobar_browser = setupBrowser(auth="Basic foo.bar@canonical.com:test")
1507- >>> foobar_browser.open('http://code.launchpad.dev/~name12/gnome-terminal'
1508- ... '/klingon/+merge/1')
1509+ >>> foobar_browser.open(klingon_proposal)
1510 >>> foobar_browser.getControl('Claim review').click()
1511 >>> pending = find_tag_by_id(
1512 ... foobar_browser.contents, 'code-review-votes')
1513@@ -230,8 +224,7 @@
1514 >>> foobar_browser.getLink('Reassign').click()
1515 >>> foobar_browser.getControl('Reviewer').value = 'hwdb-team'
1516 >>> foobar_browser.getControl('Reassign').click()
1517- >>> foobar_browser.open('http://code.launchpad.dev/~name12/gnome-terminal'
1518- ... '/klingon/+merge/1')
1519+ >>> foobar_browser.open(klingon_proposal)
1520 >>> pending = find_tag_by_id(
1521 ... foobar_browser.contents, 'code-review-votes')
1522
1523@@ -248,8 +241,7 @@
1524 When a branch has been merged into the target branch, the proposal should
1525 be marked as merged.
1526
1527- >>> nopriv_browser.open(
1528- ... 'http://code.launchpad.dev/~name12/gnome-terminal/klingon/+merge/1')
1529+ >>> nopriv_browser.open(klingon_proposal)
1530
1531 The edit icon at the end of the status allows the user to edit the status.
1532
1533@@ -264,8 +256,6 @@
1534 >>> nopriv_browser.getControl(name='field.queue_status').displayValue = (
1535 ... ['Merged'])
1536 >>> nopriv_browser.getControl('Change Status').click()
1537- >>> print nopriv_browser.url
1538- http://code.launchpad.dev/~name12/gnome-terminal/klingon/+merge/1
1539
1540 The proposal is now shown as have being merged.
1541
1542@@ -282,6 +272,7 @@
1543 >>> print extract_text(find_tag_by_id(
1544 ... sample_browser.contents, 'landing-targets'))
1545 Merged into lp://dev/~name12/gnome-terminal/main
1546+ ...
1547 >>> sample_browser.getLink('Merged').click()
1548 >>> print_summary(sample_browser)
1549 Status: Merged
1550@@ -303,6 +294,7 @@
1551 >>> print extract_text(find_tag_by_id(
1552 ... sample_browser.contents, 'landing-targets'))
1553 Merged into lp://dev/~name12/gnome-terminal/main at revision 42
1554+ ...
1555
1556
1557 Resubmitting proposals
1558@@ -315,9 +307,7 @@
1559 branches but with the state set to work-in-progress.
1560
1561 >>> login('foo.bar@canonical.com')
1562- >>> fooix = factory.makeProduct(name='fooix')
1563- >>> target = factory.makeProductBranch(product=fooix, owner=eric)
1564- >>> bmp = factory.makeBranchMergeProposal(target_branch=target)
1565+ >>> bmp = factory.makeBranchMergeProposal(target_branch=trunk)
1566 >>> bmp_url = canonical_url(bmp)
1567 >>> logout()
1568 >>> eric_browser.open(bmp_url)
1569@@ -538,7 +528,6 @@
1570 Bug #...: Bug for linking (Undecided &ndash; New)
1571
1572
1573-
1574 Target branch edge cases
1575 ------------------------
1576
1577@@ -626,3 +615,30 @@
1578 >>> print extract_text(get_review_diff())
1579 Preview Diff
1580 Empty
1581+
1582+
1583+Merge proposal details shown on the branch page
1584+-----------------------------------------------
1585+
1586+A branch that has a merge proposal, but no requested reviews shows this on the
1587+branch page.
1588+
1589+ >>> nopriv_browser.open('http://code.launchpad.dev/~fred/fooix/feature')
1590+ >>> nopriv_browser.getLink('Propose for merging').click()
1591+ >>> nopriv_browser.getControl('Reviewer').value = ''
1592+ >>> nopriv_browser.getControl('Propose Merge').click()
1593+ >>> nopriv_browser.getLink('lp://dev/~fred/fooix/feature').click()
1594+ >>> print_tag_with_id(nopriv_browser.contents, 'landing-targets')
1595+ Ready for review for merging into lp://dev/fooix
1596+ No reviews requested
1597+
1598+If there are reviews either pending or completed these are also shown.
1599+
1600+The api tag is a hidden anchor that holds the URL for the merge proposal api
1601+access.
1602+
1603+ >>> nopriv_browser.open('http://code.launchpad.dev/~fred/fooix/proposed')
1604+ >>> print_tag_with_id(nopriv_browser.contents, 'landing-targets')
1605+ Ready for review for merging into lp://dev/fooix
1606+ Eric the Viking: Pending (code) requested ... ago
1607+ Diff: 47 lines (+7/-0) 2 files modified api
1608
1609=== modified file 'lib/lp/code/templates/branch-index.pt'
1610--- lib/lp/code/templates/branch-index.pt 2009-10-11 02:33:30 +0000
1611+++ lib/lp/code/templates/branch-index.pt 2009-11-18 15:05:39 +0000
1612@@ -25,6 +25,20 @@
1613 #download-url dd span.branch-url, #upload-url dd span.branch-url {
1614 font-weight: normal;
1615 }
1616+ dl.reviews dd {
1617+ padding-left: 2em;
1618+ margin-bottom: 0;
1619+ }
1620+ .yui-diff-overlay-hidden {
1621+ display: none;
1622+ }
1623+ .yui-diff-overlay {
1624+ width: 80%;
1625+ }
1626+ .yui-diff-overlay #yui-pretty-overlay-modal {
1627+ width: auto;
1628+ color: black;
1629+ }
1630
1631 </style>
1632 <script type="text/javascript"
1633@@ -43,12 +57,20 @@
1634 tal:attributes="src string:${lp_js}/code/branchstatus.js">
1635 </script>
1636 <script type="text/javascript"
1637+ tal:condition="devmode"
1638+ tal:define="lp_js string:${icingroot}/build"
1639+ tal:attributes="src string:${lp_js}/code/popupdiff.js">
1640+ </script>
1641+ <script type="text/javascript"
1642 tal:content="string:
1643 YUI().use('node', 'event', 'widget', 'plugin', 'overlay',
1644- 'lazr.choiceedit', 'code.branchstatus', function(Y) {
1645+ 'lazr.choiceedit', 'code.branchstatus',
1646+ 'code.branchmergeproposal.popupdiff',
1647+ function(Y) {
1648 Y.on('load',
1649 function(e) {
1650 Y.branchstatus.connect_status(${view/status_config});
1651+ Y.code.branchmergeproposal.popupdiff.connect_diff_links();
1652 },
1653 window);
1654 });
1655
1656=== modified file 'lib/lp/code/templates/branch-pending-merges.pt'
1657--- lib/lp/code/templates/branch-pending-merges.pt 2009-09-18 21:20:42 +0000
1658+++ lib/lp/code/templates/branch-pending-merges.pt 2009-11-18 15:05:38 +0000
1659@@ -36,6 +36,7 @@
1660 <div>
1661 <tal:merge-fragment tal:replace="structure mergeproposal/@@+link-summary" />
1662 </div>
1663+ <tal:merge-fragment tal:replace="structure mergeproposal/@@+vote-summary" />
1664 </tal:landing-candidates>
1665 </div>
1666
1667
1668=== added file 'lib/lp/code/templates/branchmergeproposal-diff.pt'
1669--- lib/lp/code/templates/branchmergeproposal-diff.pt 1970-01-01 00:00:00 +0000
1670+++ lib/lp/code/templates/branchmergeproposal-diff.pt 2009-11-18 15:05:39 +0000
1671@@ -0,0 +1,33 @@
1672+<tal:root
1673+ xmlns:tal="http://xml.zope.org/namespaces/tal"
1674+>
1675+
1676+ <div class="boardComment attachment diff"
1677+ tal:define="attachment view/preview_diff/diff_text">
1678+ <tal:real-diff condition="attachment">
1679+ <div class="boardCommentDetails filename">
1680+ <ul class="horizontal">
1681+ <li>
1682+ <a tal:attributes="href attachment/getURL">
1683+ <img src="/@@/download"/>
1684+ Download diff
1685+ </a>
1686+ </li>
1687+ </ul>
1688+ </div>
1689+ <div class="boardCommentBody attachmentBody">
1690+ <tal:diff replace="structure view/preview_diff_text/fmt:diff" />
1691+ </div>
1692+ <div class="boardCommentFooter"
1693+ tal:condition="view/diff_oversized">
1694+ The diff has been truncated for viewing.
1695+ </div>
1696+ </tal:real-diff>
1697+ <tal:empty-diff condition="not: attachment">
1698+ <div class="boardCommentBody attachmentBody">
1699+ <em>Empty</em>
1700+ </div>
1701+ </tal:empty-diff>
1702+ </div>
1703+
1704+</tal:root>
1705\ No newline at end of file
1706
1707=== modified file 'lib/lp/code/templates/branchmergeproposal-index.pt'
1708--- lib/lp/code/templates/branchmergeproposal-index.pt 2009-11-10 21:49:38 +0000
1709+++ lib/lp/code/templates/branchmergeproposal-index.pt 2009-11-18 15:05:38 +0000
1710@@ -128,34 +128,8 @@
1711 </div>
1712
1713 <div id="review-diff" tal:condition="view/preview_diff">
1714- <h2>Preview Diff</h2>
1715- <div class="boardComment attachment"
1716- tal:define="attachment view/preview_diff/diff_text">
1717- <tal:real-diff condition="attachment">
1718- <div class="boardCommentDetails filename">
1719- <ul class="horizontal">
1720- <li>
1721- <a tal:attributes="href attachment/getURL">
1722- <img src="/@@/download"/>
1723- Download diff
1724- </a>
1725- </li>
1726- </ul>
1727- </div>
1728- <div class="boardCommentBody attachmentBody">
1729- <tal:diff replace="structure view/preview_diff_text/fmt:diff" />
1730- </div>
1731- <div class="boardCommentFooter"
1732- tal:condition="view/review_diff_oversized">
1733- The diff has been truncated for viewing.
1734- </div>
1735- </tal:real-diff>
1736- <tal:empty-diff condition="not: attachment">
1737- <div class="boardCommentBody attachmentBody">
1738- <em>Empty</em>
1739- </div>
1740- </tal:empty-diff>
1741- </div>
1742+ <h2>Preview Diff</h2>
1743+ <div tal:replace="structure context/@@++diff"/>
1744 </div>
1745
1746 <tal:script
1747
1748=== renamed file 'lib/lp/code/templates/branch-merge-link-summary.pt' => 'lib/lp/code/templates/branchmergeproposal-link-summary.pt'
1749--- lib/lp/code/templates/branch-merge-link-summary.pt 2009-09-22 16:31:42 +0000
1750+++ lib/lp/code/templates/branchmergeproposal-link-summary.pt 2009-11-18 15:05:39 +0000
1751@@ -4,7 +4,8 @@
1752 xmlns:i18n="http://xml.zope.org/namespaces/i18n">
1753
1754 <img src="/@@/merge-proposal-icon" />
1755- <a tal:attributes="href context/fmt:url"
1756+ <a tal:attributes="href context/fmt:url;
1757+ title view/status_title"
1758 tal:content="view/friendly_text">Approved</a>
1759 <tal:for-merging condition="not: context/queue_status/enumvalue:MERGED">
1760 for merging
1761
1762=== added file 'lib/lp/code/templates/branchmergeproposal-vote-summary.pt'
1763--- lib/lp/code/templates/branchmergeproposal-vote-summary.pt 1970-01-01 00:00:00 +0000
1764+++ lib/lp/code/templates/branchmergeproposal-vote-summary.pt 2009-11-18 15:05:38 +0000
1765@@ -0,0 +1,52 @@
1766+<tal:root
1767+ xmlns:tal="http://xml.zope.org/namespaces/tal"
1768+ xmlns:metal="http://xml.zope.org/namespaces/metal"
1769+ xmlns:i18n="http://xml.zope.org/namespaces/i18n">
1770+
1771+ <tal:comment condition="nothing">
1772+ <!--
1773+ Yet again we are bitten by white space issues, so some tags in this
1774+ template have closing brackets on following lines, and not breaks
1775+ between some tags.
1776+ -->
1777+ </tal:comment>
1778+
1779+<dl class="reviews">
1780+ <dd tal:condition="not: view/reviews">
1781+ No reviews requested
1782+ </dd>
1783+ <dd tal:repeat="review view/current_reviews">
1784+ <tal:reviewer replace="structure review/reviewer/fmt:link:mainsite">
1785+ Eric the Reviewer</tal:reviewer
1786+ ><tal:community condition="not: review/trusted">
1787+ (community)</tal:community>:
1788+ <span tal:attributes="class string:vote${review/comment/vote/name}"
1789+ tal:content="review/status_text">
1790+ Approved
1791+ </span>
1792+ <tal:vote-tags condition="review/review_type_str">
1793+ (<tal:tag replace="review/review_type_str"/>)
1794+ </tal:vote-tags>
1795+ <tal:date replace="review/date_of_comment/fmt:displaydate" />
1796+ </dd>
1797+ <dd tal:repeat="review view/requested_reviews"
1798+ tal:attributes="id string:review-${review/reviewer/name}">
1799+ <tal:reviewer
1800+ tal:replace="structure review/reviewer/fmt:link:mainsite" />:
1801+
1802+ <span class="votePENDING">Pending</span>
1803+ <tal:vote-tags condition="review/review_type_str">
1804+ (<tal:tag replace="review/review_type_str"/>)
1805+ </tal:vote-tags>
1806+ requested
1807+ <tal:date replace="review/date_requested/fmt:approximatedate"/>
1808+ </dd>
1809+ <dd tal:condition="context/preview_diff"
1810+ tal:attributes="class string:popup-diff mp-${context/id}">
1811+ Diff: <tal:diff replace="structure context/preview_diff/fmt:link"/>
1812+ <a class="api-ref" style="display:none"
1813+ tal:attributes="href context/preview_diff/fmt:api_url">api</a>
1814+ </dd>
1815+
1816+</dl>
1817+</tal:root>
1818
1819=== modified file 'lib/lp/code/tests/helpers.py'
1820--- lib/lp/code/tests/helpers.py 2009-10-26 18:40:04 +0000
1821+++ lib/lp/code/tests/helpers.py 2009-11-18 15:05:38 +0000
1822@@ -6,10 +6,12 @@
1823 __metaclass__ = type
1824 __all__ = [
1825 'make_linked_package_branch',
1826+ 'make_erics_fooix_project',
1827 ]
1828
1829
1830 from datetime import timedelta
1831+from difflib import unified_diff
1832 from itertools import count
1833
1834 from zope.component import getUtility
1835@@ -23,6 +25,46 @@
1836 from lp.testing import time_counter
1837
1838
1839+def make_erics_fooix_project(factory):
1840+ """Make Eric, the Fooix project, and some branches.
1841+
1842+ :return: a dict of objects to put into local scope.
1843+ """
1844+ result = {}
1845+ eric = factory.makePerson(
1846+ name='eric', displayname='Eric the Viking',
1847+ email='eric@example.com', password='test')
1848+ fooix = factory.makeProduct(
1849+ name='fooix', displayname='Fooix', owner=eric)
1850+ trunk = factory.makeProductBranch(
1851+ owner=eric, product=fooix, name='trunk')
1852+ removeSecurityProxy(fooix.development_focus).branch = trunk
1853+ # Development is done by Fred.
1854+ fred = factory.makePerson(
1855+ name='fred', displayname='Fred Flintstone',
1856+ email='fred@example.com', password='test')
1857+ feature = factory.makeProductBranch(
1858+ owner=fred, product=fooix, name='feature')
1859+ proposed = factory.makeProductBranch(
1860+ owner=fred, product=fooix, name='proposed')
1861+ bmp = proposed.addLandingTarget(
1862+ registrant=fred, target_branch=trunk, needs_review=True,
1863+ review_requests=[(eric, 'code')])
1864+ # And fake a diff.
1865+ naked_bmp = removeSecurityProxy(bmp)
1866+ preview = removeSecurityProxy(naked_bmp.updatePreviewDiff(
1867+ ''.join(unified_diff('', 'random content')), u'rev-a', u'rev-b'))
1868+ naked_bmp.source_branch.last_scanned_id = preview.source_revision_id
1869+ naked_bmp.target_branch.last_scanned_id = preview.target_revision_id
1870+ preview.diff_lines_count = 47
1871+ preview.added_lines_count = 7
1872+ preview.remvoed_lines_count = 13
1873+ preview.diffstat = {'file1': (3, 8), 'file2': (4, 5)}
1874+ return {
1875+ 'eric': eric, 'fooix': fooix, 'trunk':trunk, 'feature': feature,
1876+ 'proposed': proposed, 'fred': fred}
1877+
1878+
1879 def make_linked_package_branch(factory, distribution=None,
1880 sourcepackagename=None):
1881 """Make a new package branch and make it official."""
1882
1883=== modified file 'lib/lp/code/tests/test_directbranchcommit.py'
1884--- lib/lp/code/tests/test_directbranchcommit.py 2009-10-02 09:48:30 +0000
1885+++ lib/lp/code/tests/test_directbranchcommit.py 2009-11-18 15:05:39 +0000
1886@@ -37,7 +37,7 @@
1887
1888 self.committer = DirectBranchCommit(self.db_branch)
1889 if update_last_scanned_id:
1890- self.db_branch.last_scanned_id = (
1891+ self.committer.last_scanned_id = (
1892 self.committer.bzrbranch.last_revision())
1893
1894 def _tearDownCommitter(self):
1895
1896=== added file 'lib/lp/code/windmill/tests/test_popup_diff.py'
1897--- lib/lp/code/windmill/tests/test_popup_diff.py 1970-01-01 00:00:00 +0000
1898+++ lib/lp/code/windmill/tests/test_popup_diff.py 2009-11-18 15:05:39 +0000
1899@@ -0,0 +1,64 @@
1900+# Copyright 2009 Canonical Ltd. This software is licensed under the
1901+# GNU Affero General Public License version 3 (see the file LICENSE).
1902+
1903+"""Test for the popup diff."""
1904+
1905+__metaclass__ = type
1906+__all__ = []
1907+
1908+import transaction
1909+import unittest
1910+
1911+import windmill
1912+from windmill.authoring import WindmillTestClient
1913+
1914+from canonical.launchpad.windmill.testing.constants import PAGE_LOAD
1915+from lp.code.tests.helpers import make_erics_fooix_project
1916+from lp.code.windmill.testing import CodeWindmillLayer
1917+from lp.testing import TestCaseWithFactory
1918+
1919+
1920+POPUP_DIFF = (
1921+ u'//dd[contains(@class, "popup-diff")]'
1922+ '/a[contains(@class, "js-action")]')
1923+VISIBLE_DIFF = (
1924+ u'//table[contains(@class, "yui-diff-overlay") and '
1925+ 'not(contains(@class, "yui-diff-overlay-hidden"))]')
1926+CLOSE_VISIBLE_DIFF = (
1927+ u'//table[contains(@class, "yui-diff-overlay")]'
1928+ '//a[@class="close-button"]')
1929+JS_ONLOAD_EXECUTE_DELAY = 1000
1930+
1931+
1932+class TestPopupOnBranchPage(TestCaseWithFactory):
1933+ """Test the popup diff."""
1934+
1935+ layer = CodeWindmillLayer
1936+
1937+ def test_branch_popup_diff(self):
1938+ """Test branch diff popups."""
1939+ client = WindmillTestClient("Branch popup diffs")
1940+ make_erics_fooix_project(self.factory)
1941+ transaction.commit()
1942+
1943+ start_url = (
1944+ windmill.settings['TEST_URL'] + '~fred/fooix/proposed')
1945+ client.open(url=start_url)
1946+ client.waits.forPageLoad(timeout=PAGE_LOAD)
1947+ # Sleep for a bit to make sure that the JS onload has had time to execute.
1948+ client.waits.sleep(milliseconds=JS_ONLOAD_EXECUTE_DELAY)
1949+
1950+ # Make sure that the link anchor has the js-action class.
1951+ client.asserts.assertNode(xpath=POPUP_DIFF)
1952+ client.click(xpath=POPUP_DIFF)
1953+
1954+ # Wait for the diff to show.
1955+ client.waits.forElement(xpath=VISIBLE_DIFF)
1956+ # Click on the close button.
1957+ client.click(xpath=CLOSE_VISIBLE_DIFF)
1958+ # Make sure that the diff has gone.
1959+ client.asserts.assertNotNode(xpath=VISIBLE_DIFF)
1960+
1961+
1962+def test_suite():
1963+ return unittest.TestLoader().loadTestsFromName(__name__)
1964
1965=== modified file 'lib/lp/services/browser_helpers.py'
1966--- lib/lp/services/browser_helpers.py 2009-09-07 14:54:34 +0000
1967+++ lib/lp/services/browser_helpers.py 2009-11-18 15:05:39 +0000
1968@@ -6,6 +6,7 @@
1969 __metaclass__ = type
1970 __all__ = [
1971 'get_user_agent_distroseries',
1972+ 'get_plural_text',
1973 ]
1974
1975 import re
1976@@ -28,3 +29,10 @@
1977 else:
1978 return None
1979
1980+
1981+def get_plural_text(count, singular, plural):
1982+ """Return 'singular' if 'count' is 1, 'plural' otherwise."""
1983+ if count == 1:
1984+ return singular
1985+ else:
1986+ return plural
1987
1988=== modified file 'lib/lp/translations/doc/translations-export-to-branch.txt'
1989--- lib/lp/translations/doc/translations-export-to-branch.txt 2009-10-20 06:03:46 +0000
1990+++ lib/lp/translations/doc/translations-export-to-branch.txt 2009-11-18 15:05:39 +0000
1991@@ -45,7 +45,7 @@
1992 ... if self.simulate_race:
1993 ... raise ConcurrentUpdateError("Simulated race condition.")
1994 ...
1995- ... def commit(self, message=None):
1996+ ... def commit(self, message=None, txn=None):
1997 ... self._checkForRace()
1998 ... self.logger.info("Committed %d file(s)." % self.written_files)
1999 ...
2000@@ -129,11 +129,11 @@
2001
2002 >>> transaction.commit()
2003 >>> script = MockExportTranslationsToBranch(
2004- ... 'export-to-branch', test_args=['-vv'])
2005+ ... 'export-to-branch', test_args=[])
2006 >>> script.main()
2007 INFO Exporting to translations branches.
2008 INFO Exporting Gazblachko trunk series.
2009- DEBUG No previous translations commit found.
2010+ DEBUG ...
2011 INFO Writing file 'po/main/nl.po':
2012 INFO # ...
2013 msgid ""
2014@@ -143,6 +143,7 @@
2015 msgid "maingazpot msgid"
2016 msgstr "maingazpot msgstr"
2017 <BLANKLINE>
2018+ DEBUG ...
2019 INFO Writing file 'po/module/nl.po':
2020 INFO # ...
2021 msgid ""
2022@@ -152,6 +153,8 @@
2023 <BLANKLINE>
2024 msgid "gazmod msgid"
2025 msgstr "gazmod msgstr"
2026+ <BLANKLINE>
2027+ DEBUG ...
2028 INFO Committed 2 file(s).
2029 INFO Unlock.
2030 INFO Processed 1 item(s); 0 failure(s).
2031@@ -186,6 +189,7 @@
2032 >>> script.main()
2033 INFO Exporting to translations branches.
2034 INFO Exporting Gazblachko trunk series.
2035+ DEBUG ....
2036 DEBUG Last commit was at ....
2037 INFO Unlock.
2038 INFO Processed 1 item(s); 0 failure(s).
2039@@ -197,6 +201,7 @@
2040 >>> script.main()
2041 INFO Exporting to translations branches.
2042 INFO Exporting Gazblachko trunk series.
2043+ DEBUG ....
2044 DEBUG Last commit was at ...
2045 INFO Writing file 'po/main/nl.po':
2046 INFO ...
2047@@ -228,11 +233,14 @@
2048 >>> script.main()
2049 INFO Exporting to translations branches.
2050 INFO Exporting Gazblachko trunk series.
2051+ DEBUG ....
2052 DEBUG No previous translations commit found.
2053+ DEBUG ....
2054 INFO Writing file 'po/main/nl.po':
2055 ...
2056 msgstr "gazmod msgstr"
2057 <BLANKLINE>
2058+ DEBUG ...
2059 INFO Unlock.
2060 ERROR Failure: Simulated race condition.
2061 INFO Processed 1 item(s); 1 failure(s).
2062
2063=== modified file 'lib/lp/translations/scripts/translations_to_branch.py'
2064--- lib/lp/translations/scripts/translations_to_branch.py 2009-10-20 06:03:46 +0000
2065+++ lib/lp/translations/scripts/translations_to_branch.py 2009-11-18 15:05:39 +0000
2066@@ -119,7 +119,7 @@
2067 def _commit(self, source, committer):
2068 """Commit changes to branch. Check for race conditions."""
2069 self._checkForObjections(source)
2070- committer.commit(self.commit_message)
2071+ committer.commit(self.commit_message, txn=self.txn)
2072
2073 def _isTranslationsCommit(self, revision):
2074 """Is `revision` an automatic translations commit?"""
2075@@ -159,6 +159,9 @@
2076 self._checkForObjections(source)
2077
2078 committer = self._prepareBranchCommit(source.translations_branch)
2079+ self.logger.debug("Created DirectBranchCommit.")
2080+ if self.txn:
2081+ self.txn.commit()
2082
2083 bzr_branch = committer.bzrbranch
2084
2085@@ -185,15 +188,17 @@
2086 base_path = os.path.dirname(template.path)
2087
2088 for pofile in template.pofiles:
2089-
2090 has_changed = (
2091 changed_since is None or
2092 pofile.date_changed > changed_since)
2093 if not has_changed:
2094 continue
2095
2096+ language_code = pofile.getFullLanguageCode()
2097+ self.logger.debug("Exporting %s." % language_code)
2098+
2099 pofile_path = os.path.join(
2100- base_path, pofile.getFullLanguageCode() + '.po')
2101+ base_path, language_code + '.po')
2102 pofile_contents = pofile.export()
2103
2104 committer.writeFile(pofile_path, pofile_contents)
2105@@ -210,6 +215,7 @@
2106 template.clearPOFileCache()
2107
2108 if change_count > 0:
2109+ self.logger.debug("Writing to branch.")
2110 self._commit(source, committer)
2111 finally:
2112 committer.unlock()
2113@@ -231,13 +237,14 @@
2114 raise
2115 except Exception, e:
2116 items_failed += 1
2117- self.logger.error("Failure: %s" % e)
2118+ message = unicode(e)
2119+ if message == u'':
2120+ message = e.__class__.__name__
2121+ self.logger.error("Failure: %s" % message)
2122 if self.txn:
2123 self.txn.abort()
2124
2125 items_done += 1
2126- if self.txn:
2127- self.txn.begin()
2128
2129 self.logger.info("Processed %d item(s); %d failure(s)." % (
2130 items_done, items_failed))
2131
2132=== modified file 'lib/lp/translations/utilities/gettext_po_exporter.py'
2133--- lib/lp/translations/utilities/gettext_po_exporter.py 2009-11-02 00:10:05 +0000
2134+++ lib/lp/translations/utilities/gettext_po_exporter.py 2009-11-18 15:05:39 +0000
2135@@ -14,6 +14,7 @@
2136 'GettextPOExporter',
2137 ]
2138
2139+import logging
2140 import os
2141
2142 from zope.interface import implements
2143@@ -344,8 +345,20 @@
2144 raise UnicodeEncodeError(
2145 '%s:\n%s' % (file_path, str(error)))
2146
2147- # This message cannot be represented in current encoding,
2148- # change to UTF-8 and try again.
2149+ # This message cannot be represented in the current
2150+ # encoding.
2151+ if translation_file.path:
2152+ file_description = translation_file.path
2153+ elif translation_file.language_code:
2154+ file_description = (
2155+ "%s translation" % translation_file.language_code)
2156+ else:
2157+ file_description = "template"
2158+ logging.info(
2159+ "Can't represent %s as %s; using UTF-8 instead." % (
2160+ file_description,
2161+ translation_file.header.charset.upper()))
2162+
2163 old_charset = translation_file.header.charset
2164 translation_file.header.charset = 'UTF-8'
2165 # We need to update the header too.

Subscribers

People subscribed via source and target branches

to status/vote changes: