Merge lp:~jtv/launchpad/bug-427278 into lp:launchpad/db-devel
- bug-427278
- Merge into 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 | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Barry Warsaw (community) | Abstain | ||
Review via email: mp+14994@code.launchpad.net |
Commit message
Description of the change
To post a comment you must log in.
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote : | # |
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"/> %(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"/> 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"/> 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"/> 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"/> 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"/> 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"/> 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"/> 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 <b>mojo</b> |
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 – 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. |
= 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 DirectBranchCom mit.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: *to.*branch' -t 'direct. *branch. *commit'
{{{
./bin/test -vv -t 'translations.
}}}
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