Merge lp:~mbp/launchpad/mbp-trivial into lp:launchpad/db-devel

Proposed by Martin Pool
Status: Rejected
Rejected by: Martin Pool
Proposed branch: lp:~mbp/launchpad/mbp-trivial
Merge into: lp:launchpad/db-devel
Diff against target: 2093 lines (+284/-968)
39 files modified
database/schema/security.cfg (+1/-1)
lib/canonical/launchpad/doc/location-widget.txt (+2/-6)
lib/canonical/launchpad/webapp/servers.py (+1/-5)
lib/canonical/widgets/location.py (+7/-3)
lib/canonical/widgets/templates/location.pt (+7/-28)
lib/lp/app/javascript/mapping.js (+0/-365)
lib/lp/app/templates/base-layout-macros.pt (+0/-10)
lib/lp/code/browser/branchmergeproposal.py (+8/-42)
lib/lp/code/browser/tests/test_branchmergeproposal.py (+0/-60)
lib/lp/code/configure.zcml (+2/-0)
lib/lp/code/interfaces/branchmergeproposal.py (+9/-2)
lib/lp/code/interfaces/revision.py (+3/-0)
lib/lp/code/model/branch.py (+2/-2)
lib/lp/code/model/branchmergeproposal.py (+40/-1)
lib/lp/code/model/directbranchcommit.py (+5/-2)
lib/lp/code/model/revision.py (+8/-1)
lib/lp/code/model/tests/test_branch.py (+19/-5)
lib/lp/code/model/tests/test_branchmergeproposal.py (+67/-4)
lib/lp/code/model/tests/test_diff.py (+15/-16)
lib/lp/code/tests/helpers.py (+7/-2)
lib/lp/code/tests/test_directbranchcommit.py (+18/-0)
lib/lp/codehosting/branchdistro.py (+2/-1)
lib/lp/codehosting/bzrutils.py (+11/-0)
lib/lp/codehosting/tests/test_branchdistro.py (+7/-0)
lib/lp/registry/browser/__init__.py (+0/-18)
lib/lp/registry/browser/configure.zcml (+1/-6)
lib/lp/registry/browser/person.py (+10/-57)
lib/lp/registry/browser/team.py (+2/-12)
lib/lp/registry/browser/tests/person-views.txt (+0/-107)
lib/lp/registry/browser/tests/team-views.txt (+5/-28)
lib/lp/registry/doc/personlocation.txt (+4/-2)
lib/lp/registry/stories/location/personlocation-edit.txt (+2/-33)
lib/lp/registry/stories/location/personlocation.txt (+0/-40)
lib/lp/registry/stories/location/team-map.txt (+0/-53)
lib/lp/registry/templates/person-editlocation.pt (+0/-23)
lib/lp/registry/templates/person-portlet-map.pt (+0/-25)
lib/lp/registry/templates/team-index.pt (+0/-3)
lib/lp/registry/templates/team-portlet-map.pt (+1/-2)
test_on_merge.py (+18/-3)
To merge this branch: bzr merge lp:~mbp/launchpad/mbp-trivial
Reviewer Review Type Date Requested Status
Robert Collins (community) Approve
Review via email: mp+32173@code.launchpad.net

Description of the change

Resizing your window shouldn't break "make check"! See bug 615740 for more.

To post a comment you must log in.
Revision history for this message
Martin Pool (mbp) wrote :

lifeless and spiv gave +1s on irc, and I've manually tested before/after.

Revision history for this message
Martin Pool (mbp) wrote :

wrong: apparently they don't have an errno...

Revision history for this message
Martin Pool (mbp) wrote :

should be ok now

Revision history for this message
Robert Collins (lifeless) wrote :

nice.

review: Approve
Revision history for this message
Martin Pool (mbp) wrote :

should go to devel, not db-devel, therefore i'll obsolete this in favor of https://code.edge.launchpad.net/~mbp/launchpad/mbp-trivial/+merge/36515

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'database/schema/security.cfg'
2--- database/schema/security.cfg 2010-09-20 23:40:08 +0000
3+++ database/schema/security.cfg 2010-09-22 06:47:48 +0000
4@@ -629,7 +629,7 @@
5
6 [branch-distro]
7 type=user
8-public.branch = SELECT, INSERT
9+public.branch = SELECT, INSERT, UPDATE
10 public.branchsubscription = SELECT, INSERT
11 public.distribution = SELECT
12 public.distroseries = SELECT
13
14=== modified file 'lib/canonical/launchpad/doc/location-widget.txt'
15--- lib/canonical/launchpad/doc/location-widget.txt 2010-08-11 15:47:27 +0000
16+++ lib/canonical/launchpad/doc/location-widget.txt 2010-09-22 06:47:48 +0000
17@@ -9,22 +9,18 @@
18 >>> salgado = getUtility(IPersonSet).getByName('salgado')
19 >>> field = LocationField(__name__='location', title=u'Location')
20
21-JavaScript and JSON are used in the Google Maps API. By setting the
22-needs_gmap2 and needs_json attributes of the request, the main template
23+JavaScript and JSON are used by the location widget. By setting the
24+needs_json attributes of the request, the main template
25 will include the necessary code to enable these features.
26
27 >>> bound_field = field.bind(salgado)
28 >>> request = LaunchpadTestRequest(
29 ... # Let's pretend requests are coming from Brazil.
30 ... environ={'REMOTE_ADDR': '201.13.165.145'})
31- >>> request.needs_gmap2
32- False
33 >>> request.needs_json
34 False
35
36 >>> widget = LocationWidget(bound_field, request)
37- >>> request.needs_gmap2
38- True
39 >>> request.needs_json
40 True
41
42
43=== modified file 'lib/canonical/launchpad/webapp/servers.py'
44--- lib/canonical/launchpad/webapp/servers.py 2010-09-16 21:08:51 +0000
45+++ lib/canonical/launchpad/webapp/servers.py 2010-09-22 06:47:48 +0000
46@@ -522,7 +522,6 @@
47 self.needs_datepicker_iframe = False
48 self.needs_datetimepicker_iframe = False
49 self.needs_json = False
50- self.needs_gmap2 = False
51 super(BasicLaunchpadRequest, self).__init__(
52 body_instream, environ, response)
53
54@@ -834,12 +833,10 @@
55 >>> request.needs_datepicker_iframe
56 False
57
58- And for JSON and GMap2:
59+ And for JSON:
60
61 >>> request.needs_json
62 False
63- >>> request.needs_gmap2
64- False
65
66 """
67 implements(INotificationRequest, IBasicLaunchpadRequest, IParticipation,
68@@ -857,7 +854,6 @@
69 self.needs_datepicker_iframe = False
70 self.needs_datetimepicker_iframe = False
71 self.needs_json = False
72- self.needs_gmap2 = False
73 # stub out the FeatureController that would normally be provided by
74 # the publication mechanism
75 self.features = NullFeatureController()
76
77=== modified file 'lib/canonical/widgets/location.py'
78--- lib/canonical/widgets/location.py 2010-09-11 19:25:13 +0000
79+++ lib/canonical/widgets/location.py 2010-09-22 06:47:48 +0000
80@@ -42,6 +42,7 @@
81 This is a single object which contains the latitude, longitude and time
82 zone of the location.
83 """
84+
85 def __init__(self, latitude, longitude, time_zone):
86 self.latitude = latitude
87 self.longitude = longitude
88@@ -59,13 +60,16 @@
89 # json-handling, so we flag that in the request so that our
90 # base-layout includes the necessary javascript files.
91 request.needs_json = True
92- request.needs_gmap2 = True
93 super(LocationWidget, self).__init__(context, request)
94 fields = form.Fields(
95 Float(__name__='latitude', title=_('Latitude'), required=False),
96 Float(__name__='longitude', title=_('Longitude'), required=False),
97- Choice(__name__='time_zone', vocabulary='TimezoneName',
98- title=_('Time zone'), required=True))
99+ Choice(
100+ __name__='time_zone', vocabulary='TimezoneName',
101+ title=_('Time zone'), required=True,
102+ description=_(
103+ 'Once the time zone is correctly set, events '
104+ 'in Launchpad will be displayed in local time.')))
105 # This will be the initial zoom level and center of the map.
106 self.zoom = 2
107 self.center_lat = 15.0
108
109=== modified file 'lib/canonical/widgets/templates/location.pt'
110--- lib/canonical/widgets/templates/location.pt 2009-07-17 17:59:07 +0000
111+++ lib/canonical/widgets/templates/location.pt 2010-09-22 06:47:48 +0000
112@@ -2,32 +2,11 @@
113 xmlns:tal="http://xml.zope.org/namespaces/tal"
114 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
115 omit-tag="">
116-<tal:latitude replace="structure view/latitude_widget/hidden" />
117-<tal:longitude replace="structure view/longitude_widget/hidden" />
118-<p class="formHelp">
119- You can drag the marker to change the location, and zoom into the map to
120- see more details and verify the location's accuracy.
121- If your mouse has a scroll wheel, use it to more quickly zoom the
122- map in and out. Double-clicking will also zoom in and move the
123- marker. Please <strong>do not disclose sensitive information such
124- as a specific home location</strong> without the permission of
125- the person involved - rather just indicate a city so that the time
126- zone is correct.
127-</p>
128-<p id="map_div" style="width: 100%; height: 300px; border: 1px; float: left;"
129- ></p>
130-
131-<tal:render-map replace="structure view/map_javascript" />
132-
133-<p>
134- <label>Time zone:
135- <img id="tz_spinner" src="/@@/nospin" width="14" height="14" />
136- </label>
137- <tal:latitude replace="structure view/time_zone_widget" />
138-</p>
139-
140-<p class="formHelp">
141- Once the time zone is correctly set, events in Launchpad will be
142- displayed in local time.
143-</p>
144+ <tal:latitude replace="structure view/latitude_widget/hidden" />
145+ <tal:longitude replace="structure view/longitude_widget/hidden" />
146+ <div>
147+ <tal:time-zone replace="structure view/time_zone_widget" />
148+ </div>
149+ <div class="formHelp"
150+ tal:content="view/time_zone_widget/hint" />
151 </tal:root>
152
153=== removed file 'lib/lp/app/javascript/mapping.js'
154--- lib/lp/app/javascript/mapping.js 2010-07-15 10:55:27 +0000
155+++ lib/lp/app/javascript/mapping.js 1970-01-01 00:00:00 +0000
156@@ -1,365 +0,0 @@
157-/**
158- * Launchpad mapping tools.
159- *
160- * Map rendering and marker creation depends on the Google GMap2 library.
161- *
162- * @module lp.app.mapping
163- * @namespace lp.app.mapping
164- * @required Google GMap2
165- */
166-YUI.add('lp.app.mapping', function(Y) {
167- var module = Y.namespace('lp.app.mapping');
168-
169- module.RETURN_FALSE = function() {return false;};
170- module.RETURN_NULL = function() {return null;};
171-
172- // Replace the crucial GMap functions so that the supporting functions
173- // will work if the GMap script is not loaded.
174- var gBrowserIsCompatible = module.RETURN_FALSE;
175- var gDownloadUrl = module.RETURN_NULL;
176-
177- module.has_gmaps = (typeof(GBrowserIsCompatible) == 'function');
178-
179- if (module.has_gmaps) {
180- // The GMap2 is is loaded; use the real functions.
181- // jslint does not like functions that look like classes.
182- gBrowserIsCompatible = GBrowserIsCompatible;
183- gDownloadUrl = GDownloadUrl;
184- }
185-
186-
187- /**
188- * Add a marker for each participant.
189- *
190- * @function setMarkersInfoWindow
191- * @param {String} data the participant XML.
192- * @param {GMap2} map the Google map to add the markers to.
193- * @param {GLatLngBounds} required_bounds the boundaries or null.
194- * @param {limit} optional max number of markers to set.
195- */
196- module.setMarkersInfoWindow = function(data, map, required_bounds,
197- limit) {
198- var xml = GXml.parse(data);
199- var markers = xml.documentElement.getElementsByTagName("participant");
200- var participant = null;
201-
202- function attrToProp(attr) {
203- participant[attr.name] = attr.value;
204- }
205-
206- limit = typeof(limit) == 'number' ? limit : markers.length;
207- if (markers.length < limit) {
208- limit = markers.length;
209- }
210-
211- for (var i = 0; i < limit; i++) {
212- participant = {};
213- Y.Array.each(markers[i].attributes, attrToProp);
214- var point = new GLatLng(
215- parseFloat(participant.lat), parseFloat(participant.lng));
216- if (required_bounds) {
217- required_bounds.extend(point);
218- }
219- var marker = new GMarker(point);
220- marker.bindInfoWindowHtml(Y.substitute([
221- '<div style="text-align: center">',
222- '<a href="{url}">{displayname} ({name})</a><br />',
223- '{logo_html}<br />',
224- 'Local time: {local_time}</div>'].join(""),
225- participant));
226- map.addOverlay(marker);
227- }
228- };
229-
230- /**
231- * Add a marker for each participant, and update the zoom level.
232- *
233- * @function setMarkersInfoWindowForSmallMap
234- * @param {String} data the participant XML.
235- * @param {GMap2} map the Google map to add the markers to.
236- * @param {limit} optional max number of markers to set.
237- */
238- module.setMarkersInfoWindowForSmallMap = function(data, map, limit) {
239- var required_bounds = new GLatLngBounds();
240- module.setMarkersInfoWindow(data, map, required_bounds, limit);
241- var zoom_level = map.getBoundsZoomLevel(required_bounds);
242- // Some browsers do not display the map when the zoom_level is at the
243- // end of the range, reduce the zoom_level by 1.
244- zoom_level = Math.min(4, zoom_level - 1);
245- map.setZoom(zoom_level);
246- };
247-
248- /**
249- * Set the timezone field to the lat-log location.
250- *
251- * @function setLocation
252- * @param {Number} lat a GLatLng.lat bounded number.
253- * @param {Number} lng a GLatLng.lng bounded number.
254- * @parma {String} geoname the user-name to make geonames requests.
255- * @param {String} tz_name the id of the timezone field.
256- * @param {String} lat_name the id of the latitude field.
257- * @param {String} lng_name the id of the longitude field.
258- */
259- module.setLocation = function(lat, lng, geoname,
260- tz_name, lat_name, lng_name) {
261- Y.one(Y.DOM.byId(lat_name)).set('value', lat);
262- Y.one(Y.DOM.byId(lng_name)).set('value', lng);
263- var spinner = Y.one('#tz_spinner');
264- spinner.set('src', '/@@/spinner');
265-
266- function succeeded() {
267- if (request.readyState == 4) {
268- if (request.responseText) {
269- var tz = request.responseJSON.timezoneId;
270- Y.one(Y.DOM.byId(tz_name)).set('value', tz);
271- spinner.set('src', '/@@/nospin');
272- }
273- }
274- }
275-
276- var url = 'http://ba-ws.geonames.net/timezoneJSON' +
277- '?username=' + geoname + '&lat=' + lat + '&lng=' + lng;
278- // This is a cross-site script request.
279- var request = new JSONScriptRequest();
280- request.open("GET", url);
281- request.onreadystatechange = succeeded;
282- request.send(null);
283- };
284-
285- /**
286- * Show/hide all small maps in pages.
287- *
288- * The state is stored as ``small_maps`` in the launchpad_views cookie.
289- *
290- * @function toggleShowSmallMaps
291- * @param {Event} e the event for this callback.
292- */
293- module.toggleShowSmallMaps = function (checkbox) {
294- var is_shown = checkbox.get('checked');
295- Y.lp.launchpad_views.set('small_maps', is_shown);
296- var display = is_shown ? 'block' : 'none';
297- var maps = Y.all('.small-map');
298- maps.each(function(map) {map.setStyle('display', display);});
299- if (is_shown && !module.has_gmaps) {
300- // The server must add the Google GMap2 dependencies to the page.
301- window.location.reload();
302- }
303- };
304-
305- /**
306- * Add a checkbox to show/hide all small maps in pages.
307- *
308- * @function setupShowSmallMapsControl
309- * @param {String} div_id the CSS3 id of the div that controls the map.
310- */
311- module.setupShowSmallMapsControl = function (div_id) {
312- var show_small_maps = Y.lp.launchpad_views.get('small_maps');
313- var checkbox = Y.Node.create(
314- '<input type="checkbox" name="show_small_maps" />');
315- checkbox.set(
316- 'checked', show_small_maps);
317- checkbox.on(
318- 'click', function(e) {module.toggleShowSmallMaps(checkbox);});
319- var label_text = Y.Node.create('Display map');
320- var label = Y.Node.create('<label></label>');
321- label.appendChild(checkbox);
322- label.appendChild(label_text);
323- var action_div = Y.one(div_id);
324- action_div.appendChild(label);
325- if (!show_small_maps) {
326- module.toggleShowSmallMaps(checkbox);
327- }
328- };
329-
330- /**
331- * Create a small map with the launchpad default configuration.
332- *
333- * @function getSmallMap
334- * @param {String} div_id the id of the map div.
335- * @param {Number} center_lat a GLatLng.lat bounded number.
336- * @param {Number} center_lng a GLatLng.lng bounded number.
337- * @return {GMap2} the Google map
338- */
339- module.getSmallMap = function(div_id, center_lat, center_lng) {
340- var mapdiv = Y.DOM.byId(div_id);
341- mapdiv.style.width = '400px';
342- var map = new GMap2(mapdiv);
343- var center = new GLatLng(center_lat, center_lng);
344- map.setCenter(center, 1);
345- map.setMapType(G_NORMAL_MAP);
346- return map;
347- };
348-
349- /**
350- * Create a small map of where a person is located.
351- *
352- * @function renderPersonMapSmall
353- * @param {Number} center_lat a GLatLng.lat bounded number.
354- * @param {Number} center_lng a GLatLng.lng bounded number.
355- */
356- module.renderPersonMapSmall = function(center_lat, center_lng) {
357- module.setupShowSmallMapsControl('#person_map_actions');
358- if (!gBrowserIsCompatible()) {
359- return;
360- }
361- var map = module.getSmallMap(
362- 'person_map_div', center_lat, center_lng);
363- map.addControl(new GSmallZoomControl());
364- var center = new GLatLng(center_lat, center_lng);
365- var marker = new GMarker(center);
366- map.addOverlay(marker);
367- };
368-
369- /**
370- * Create a small map of where a team's members are located. The map is
371- * limited to 24 members.
372- *
373- * @function renderTeamMapSmall
374- * @param {Number} center_lat a GLatLng.lat bounded number.
375- * @param {Number} center_lng a GLatLng.lng bounded number.
376- */
377- module.renderTeamMapSmall = function(center_lat, center_lng) {
378- module.setupShowSmallMapsControl('#team_map_actions');
379- if (!gBrowserIsCompatible()) {
380- return;
381- }
382- var team_map = module.getSmallMap(
383- 'team_map_div', center_lat, center_lng);
384- gDownloadUrl("+mapdataltd", function(data) {
385- module.setMarkersInfoWindowForSmallMap(data, team_map);
386- });
387- };
388-
389- /**
390- * Create a large map with the launchpad default configuration.
391- *
392- * @function getLargeMap
393- * @param {String} div_id the id of the map div.
394- * @return {GMap2} The Google map
395- */
396- module.getLargeMap = function(div_id) {
397- var mapdiv = Y.DOM.byId(div_id);
398- var mapheight = (parseInt(mapdiv.offsetWidth, 10) / 16 * 9);
399- mapheight = Math.min(mapheight, Y.DOM.winHeight() - 180);
400- mapheight = Math.max(mapheight, 400);
401- mapdiv.style.height = mapheight + 'px';
402- var map = new GMap2(mapdiv);
403- map.setMapType(G_HYBRID_MAP);
404- map.addControl(new GLargeMapControl());
405- map.addControl(new GMapTypeControl());
406- map.addControl(new GScaleControl());
407- map.enableScrollWheelZoom();
408- var overview_control = new GOverviewMapControl();
409- map.addControl(overview_control);
410- GEvent.addListener(map, 'zoomend', function(old, current) {
411- try {
412- if (current < 3) {
413- overview_control.hide();
414- } else {
415- overview_control.show();
416- }
417- } catch(e) {
418- // Google removed this undocumented method.
419- }
420- });
421- return map;
422- };
423-
424- /**
425- * Create a large map of where a team's members are located.
426- *
427- * @function renderTeamMap
428- * @param {Number} min_lat the minimum GLatLng.lat bounded number.
429- * @param {Number} max_lat the maximum GLatLng.lat bounded number.
430- * @param {Number} min_lng the minimum GLatLng.lng bounded number.
431- * @param {Number} max_lng the maximum GLatLng.lng bounded number.
432- * @param {Number} center_lat a GLatLng.lat bounded number.
433- * @param {Number} center_lng a GLatLng.lng bounded number.
434- */
435- module.renderTeamMap = function(min_lat, max_lat, min_lng, max_lng,
436- center_lat, center_lng) {
437- if (!gBrowserIsCompatible()) {
438- return;
439- }
440- var team_map = module.getLargeMap("team_map_div");
441- var center = new GLatLng(center_lat, center_lng);
442- team_map.setCenter(center, 0);
443- var sw = new GLatLng(min_lat, min_lng);
444- var ne = new GLatLng(max_lat, max_lng);
445- var required_bounds = new GLatLngBounds(sw, ne);
446- var zoom_level = team_map.getBoundsZoomLevel(required_bounds);
447- // Some browsers do not display the map when the zoom_level is at
448- // the end of the range, reduce the zoom_level by 1.
449- zoom_level = Math.min(
450- G_HYBRID_MAP.getMaximumResolution(), zoom_level - 1);
451- team_map.setZoom(zoom_level);
452- gDownloadUrl("+mapdata", function(data) {
453- module.setMarkersInfoWindow(data, team_map);
454- });
455- };
456-
457- /**
458- * Create a large, markable map of where a person is located.
459- *
460- * @function renderPersonMap
461- * @param {Number} center_lat a GLatLng.lat bounded number.
462- * @param {Number} center_lng a GLatLng.lng bounded number.
463- * @param {String} displayname the user's display name.
464- * @param {String} name the user's launchpad id.
465- * @param {String} logo_html the markup to display the user's logo.
466- * @param {String} geoname the identity used to access ba-ws.geonames.net.
467- * @param {String} tz_name the id of the timezone field.
468- * @param {String} lat_name the id of the latitude field.
469- * @param {String} lng_name the id of the longitude field.
470- * @param {number} zoom the initial zoom-level.
471- * @param {Boolean} show_marker Show the marker for the person.
472- */
473- module.renderPersonMap = function(center_lat, center_lng, displayname,
474- name, logo_html, geoname, lat_name,
475- lng_name, tz_name, zoom, show_marker) {
476- if (!gBrowserIsCompatible()) {
477- return;
478- }
479- var map = module.getLargeMap('map_div');
480- var center = new GLatLng(center_lat, center_lng);
481- map.setCenter(center, zoom);
482- var marker = new GMarker(center, {draggable: true});
483- marker.bindInfoWindowHtml(Y.substitute(
484- '<div style="text-align: center">' +
485- '<strong>{displayname}</strong><br />' +
486- '{logo_html}<br />({name})</div>',
487- {displayname: displayname, logo_html: logo_html, name: name}),
488- {maxWidth: 120});
489-
490- GEvent.addListener(marker, "dragend", function() {
491- var point = marker.getLatLng();
492- module.setLocation(
493- point.lat(), point.lng(), geoname,
494- tz_name, lat_name, lng_name);
495- });
496-
497- GEvent.addListener(marker, "dragstart", function() {
498- marker.closeInfoWindow();
499- });
500-
501- map.addOverlay(marker);
502- if (!show_marker) {
503- marker.hide();
504- }
505-
506- GEvent.addListener(map, "zoomend", function() {
507- marker.closeInfoWindow();
508- });
509-
510- GEvent.addListener(map, "click", function(overlay, point) {
511- marker.setPoint(point);
512- if (marker.isHidden()) {
513- marker.show();
514- map.panTo(point);
515- }
516- module.setLocation(
517- point.lat(), point.lng(), geoname,
518- tz_name, lat_name, lng_name);
519- });
520- };
521-}, "0.1", {"requires":["node", "dom", "substitute", "lp"]});
522
523=== modified file 'lib/lp/app/templates/base-layout-macros.pt'
524--- lib/lp/app/templates/base-layout-macros.pt 2010-08-20 13:33:51 +0000
525+++ lib/lp/app/templates/base-layout-macros.pt 2010-09-22 06:47:48 +0000
526@@ -179,8 +179,6 @@
527 <script type="text/javascript"
528 tal:attributes="src string:${lp_js}/app/picker.js"></script>
529 <script type="text/javascript"
530- tal:attributes="src string:${lp_js}/app/mapping.js"></script>
531- <script type="text/javascript"
532 tal:attributes="src string:${lp_js}/bugs/bugtracker_overlay.js"></script>
533 <script type="text/javascript"
534 tal:attributes="src string:${lp_js}/registry/milestoneoverlay.js"></script>
535@@ -287,14 +285,6 @@
536 <script type="text/javascript"
537 tal:attributes="src string:${icingroot_contrib}/JSONScriptRequest.js"></script>
538 </tal:needs_json>
539- <tal:needs-gmap2 condition="request/needs_gmap2">
540- <script type="text/javascript"
541- tal:condition="devmode"
542- tal:attributes="src string:http://maps.google.com/maps?${map_query};"></script>
543- <script type="text/javascript"
544- tal:condition="not: devmode"
545- tal:attributes="src string:https://maps-api-ssl.google.com/maps?oe=utf-8&amp;client=gme-canonical${map_query};"></script>
546- </tal:needs-gmap2>
547
548 <metal:load-lavascript use-macro="context/@@+base-layout-macros/load-javascript" />
549
550
551=== modified file 'lib/lp/code/browser/branchmergeproposal.py'
552--- lib/lp/code/browser/branchmergeproposal.py 2010-08-24 10:45:57 +0000
553+++ lib/lp/code/browser/branchmergeproposal.py 2010-09-22 06:47:48 +0000
554@@ -32,7 +32,6 @@
555 'latest_proposals_for_each_branch',
556 ]
557
558-from collections import defaultdict
559 import operator
560
561 from lazr.delegates import delegates
562@@ -200,7 +199,7 @@
563 'Approved [Merge Failed]',
564 BranchMergeProposalStatus.QUEUED : 'Queued',
565 BranchMergeProposalStatus.SUPERSEDED : 'Superseded'
566- }
567+ }
568 return friendly_texts[self.context.queue_status]
569
570 @property
571@@ -212,8 +211,7 @@
572 result = ''
573 if self.context.queue_status in (
574 BranchMergeProposalStatus.CODE_APPROVED,
575- BranchMergeProposalStatus.REJECTED
576- ):
577+ BranchMergeProposalStatus.REJECTED):
578 formatter = DateTimeFormatterAPI(self.context.date_reviewed)
579 result = '%s %s' % (
580 self.context.reviewer.displayname,
581@@ -601,46 +599,15 @@
582 """Location of page for commenting on this proposal."""
583 return canonical_url(self.context, view_name='+comment')
584
585- @property
586- def revision_end_date(self):
587- """The cutoff date for showing revisions.
588-
589- If the proposal has been merged, then we stop at the merged date. If
590- it is rejected, we stop at the reviewed date. For superseded
591- proposals, it should ideally use the non-existant date_last_modified,
592- but could use the last comment date.
593- """
594- status = self.context.queue_status
595- if status == BranchMergeProposalStatus.MERGED:
596- return self.context.date_merged
597- if status == BranchMergeProposalStatus.REJECTED:
598- return self.context.date_reviewed
599- # Otherwise return None representing an open end date.
600- return None
601-
602- def _getRevisionsSinceReviewStart(self):
603- """Get the grouped revisions since the review started."""
604- # Work out the start of the review.
605- start_date = self.context.date_review_requested
606- if start_date is None:
607- start_date = self.context.date_created
608- source = DecoratedBranch(self.context.source_branch)
609- resultset = source.getMainlineBranchRevisions(
610- start_date, self.revision_end_date, oldest_first=True)
611- # Now group by date created.
612- groups = defaultdict(list)
613- for branch_revision, revision, revision_author in resultset:
614- groups[revision.date_created].append(branch_revision)
615- return [
616- CodeReviewNewRevisions(revisions, date, source)
617- for date, revisions in groups.iteritems()]
618-
619 @cachedproperty
620 def conversation(self):
621 """Return a conversation that is to be rendered."""
622 # Sort the comments by date order.
623- comments = self._getRevisionsSinceReviewStart()
624 merge_proposal = self.context
625+ groups = merge_proposal.getRevisionsSinceReviewStart()
626+ source = DecoratedBranch(merge_proposal.source_branch)
627+ comments = [CodeReviewNewRevisions(list(revisions), date, source)
628+ for date, revisions in groups]
629 while merge_proposal is not None:
630 from_superseded = merge_proposal != self.context
631 comments.extend(
632@@ -946,7 +913,6 @@
633 self.cancel_url = self.next_url
634 super(MergeProposalEditView, self).initialize()
635
636-
637 def _getRevisionId(self, data):
638 """Translate the revision number that was entered into a revision id.
639
640@@ -1473,8 +1439,8 @@
641 """Render an `IText` as XHTML using the webservice."""
642 formatter = FormattersAPI
643 def renderer(value):
644- nomail = formatter(value).obfuscate_email()
645- html = formatter(nomail).text_to_html()
646+ nomail = formatter(value).obfuscate_email()
647+ html = formatter(nomail).text_to_html()
648 return html.encode('utf-8')
649 return renderer
650
651
652=== modified file 'lib/lp/code/browser/tests/test_branchmergeproposal.py'
653--- lib/lp/code/browser/tests/test_branchmergeproposal.py 2010-08-22 21:34:16 +0000
654+++ lib/lp/code/browser/tests/test_branchmergeproposal.py 2010-09-22 06:47:48 +0000
655@@ -12,7 +12,6 @@
656 timedelta,
657 )
658 from difflib import unified_diff
659-import operator
660 import unittest
661
662 import pytz
663@@ -46,7 +45,6 @@
664 PreviewDiff,
665 StaticDiff,
666 )
667-from lp.code.tests.helpers import add_revision_to_branch
668 from lp.testing import (
669 login_person,
670 TestCaseWithFactory,
671@@ -577,63 +575,6 @@
672 view = create_initialized_view(self.bmp, '+index')
673 self.assertEqual([], view.linked_bugs)
674
675- def test_revision_end_date_active(self):
676- # An active merge proposal will have None as an end date.
677- bmp = self.factory.makeBranchMergeProposal()
678- view = create_initialized_view(bmp, '+index')
679- self.assertIs(None, view.revision_end_date)
680-
681- def test_revision_end_date_merged(self):
682- # An merged proposal will have the date merged as an end date.
683- bmp = self.factory.makeBranchMergeProposal(
684- set_state=BranchMergeProposalStatus.MERGED)
685- view = create_initialized_view(bmp, '+index')
686- self.assertEqual(bmp.date_merged, view.revision_end_date)
687-
688- def test_revision_end_date_rejected(self):
689- # An rejected proposal will have the date reviewed as an end date.
690- bmp = self.factory.makeBranchMergeProposal(
691- set_state=BranchMergeProposalStatus.REJECTED)
692- view = create_initialized_view(bmp, '+index')
693- self.assertEqual(bmp.date_reviewed, view.revision_end_date)
694-
695- def assertRevisionGroups(self, bmp, expected_groups):
696- """Get the groups for the merge proposal and check them."""
697- view = create_initialized_view(bmp, '+index')
698- groups = view._getRevisionsSinceReviewStart()
699- view_groups = [
700- obj.revisions for obj in sorted(
701- groups, key=operator.attrgetter('date'))]
702- self.assertEqual(expected_groups, view_groups)
703-
704- def test_getRevisionsSinceReviewStart_no_revisions(self):
705- # If there have been no revisions pushed since the start of the
706- # review, the method returns an empty list.
707- self.assertRevisionGroups(self.bmp, [])
708-
709- def test_getRevisionsSinceReviewStart_groups(self):
710- # Revisions that were scanned at the same time have the same
711- # date_created. These revisions are grouped together.
712- review_date = datetime(2009, 9, 10, tzinfo=pytz.UTC)
713- bmp = self.factory.makeBranchMergeProposal(
714- date_created=review_date)
715- login_person(bmp.registrant)
716- bmp.requestReview(review_date)
717- revision_date = review_date + timedelta(days=1)
718- revisions = []
719- for date in range(2):
720- revisions.append(
721- add_revision_to_branch(
722- self.factory, bmp.source_branch, revision_date))
723- revisions.append(
724- add_revision_to_branch(
725- self.factory, bmp.source_branch, revision_date))
726- revision_date += timedelta(days=1)
727- expected_groups = [
728- [revisions[0], revisions[1]],
729- [revisions[2], revisions[3]]]
730- self.assertRevisionGroups(bmp, expected_groups)
731-
732 def test_include_superseded_comments(self):
733 for x, time in zip(range(3), time_counter()):
734 if x != 0:
735@@ -757,7 +698,6 @@
736
737 layer = LaunchpadFunctionalLayer
738
739-
740 def _makeCommentFromEmailWithAttachment(self, attachment_body):
741 # Make an email message with an attachment, and create a code
742 # review comment from it.
743
744=== modified file 'lib/lp/code/configure.zcml'
745--- lib/lp/code/configure.zcml 2010-09-20 22:16:32 +0000
746+++ lib/lp/code/configure.zcml 2010-09-22 06:47:48 +0000
747@@ -236,8 +236,10 @@
748 votes
749 all_comments
750 related_bugs
751+ revision_end_date
752 isMergable
753 getComment
754+ getRevisionsSinceReviewStart
755 getNotificationRecipients
756 getVoteReference
757 isValidTransition
758
759=== modified file 'lib/lp/code/interfaces/branchmergeproposal.py'
760--- lib/lp/code/interfaces/branchmergeproposal.py 2010-08-20 20:31:18 +0000
761+++ lib/lp/code/interfaces/branchmergeproposal.py 2010-09-22 06:47:48 +0000
762@@ -290,6 +290,13 @@
763 def getComment(id):
764 """Return the CodeReviewComment with the specified ID."""
765
766+ def getRevisionsSinceReviewStart():
767+ """Return all the revisions added since the review began.
768+
769+ Revisions are grouped by creation (i.e. push) time.
770+ :return: An iterator of (date, iterator of revision data)
771+ """
772+
773 def getVoteReference(id):
774 """Return the CodeReviewVoteReference with the specified ID."""
775
776@@ -518,8 +525,8 @@
777 source branch.
778 :param target_revision_id: The revision id that was used from the
779 target branch.
780- :param prerequisite_revision_id: The revision id that was used from the
781- prerequisite branch.
782+ :param prerequisite_revision_id: The revision id that was used from
783+ the prerequisite branch.
784 :param conflicts: Text describing the conflicts if any.
785 """
786
787
788=== modified file 'lib/lp/code/interfaces/revision.py'
789--- lib/lp/code/interfaces/revision.py 2010-08-20 20:31:18 +0000
790+++ lib/lp/code/interfaces/revision.py 2010-09-22 06:47:48 +0000
791@@ -73,6 +73,9 @@
792 :return: A `Branch` or None if an appropriate branch cannot be found.
793 """
794
795+ def getLefthandParent():
796+ """Return lefthand parent of revision, or None if not in database."""
797+
798
799 class IRevisionAuthor(Interface):
800 """Committer of a Bazaar revision."""
801
802=== modified file 'lib/lp/code/model/branch.py'
803--- lib/lp/code/model/branch.py 2010-09-19 22:32:31 +0000
804+++ lib/lp/code/model/branch.py 2010-09-22 06:47:48 +0000
805@@ -990,8 +990,8 @@
806 increment = getUtility(IBranchPuller).MIRROR_TIME_INCREMENT
807 self.next_mirror_time = (
808 datetime.now(pytz.timezone('UTC')) + increment)
809- if self.last_mirrored_id != last_revision_id:
810- self.last_mirrored_id = last_revision_id
811+ self.last_mirrored_id = last_revision_id
812+ if self.last_scanned_id != last_revision_id:
813 from lp.code.model.branchjob import BranchScanJob
814 BranchScanJob.create(self)
815 self.control_format = control_format
816
817=== modified file 'lib/lp/code/model/branchmergeproposal.py'
818--- lib/lp/code/model/branchmergeproposal.py 2010-09-13 04:36:24 +0000
819+++ lib/lp/code/model/branchmergeproposal.py 2010-09-22 06:47:48 +0000
820@@ -13,7 +13,7 @@
821 ]
822
823 from email.Utils import make_msgid
824-
825+from itertools import groupby
826 from sqlobject import (
827 ForeignKey,
828 IntCol,
829@@ -777,6 +777,45 @@
830 Store.of(self).flush()
831 return self.preview_diff
832
833+ @property
834+ def revision_end_date(self):
835+ """The cutoff date for showing revisions.
836+
837+ If the proposal has been merged, then we stop at the merged date. If
838+ it is rejected, we stop at the reviewed date. For superseded
839+ proposals, it should ideally use the non-existant date_last_modified,
840+ but could use the last comment date.
841+ """
842+ status = self.queue_status
843+ if status == BranchMergeProposalStatus.MERGED:
844+ return self.date_merged
845+ if status == BranchMergeProposalStatus.REJECTED:
846+ return self.date_reviewed
847+ # Otherwise return None representing an open end date.
848+ return None
849+
850+ def _getNewerRevisions(self):
851+ start_date = self.date_review_requested
852+ if start_date is None:
853+ start_date = self.date_created
854+ return self.source_branch.getMainlineBranchRevisions(
855+ start_date, self.revision_end_date, oldest_first=True)
856+
857+ def getRevisionsSinceReviewStart(self):
858+ """Get the grouped revisions since the review started."""
859+ resultset = self._getNewerRevisions()
860+ # Work out the start of the review.
861+ branch_revisions = (
862+ branch_revision for branch_revision, revision, revision_author
863+ in resultset)
864+ # Now group by date created.
865+ gby = groupby(branch_revisions, lambda r: r.revision.date_created)
866+ # Use a generator expression to wrap the custom iterator so it doesn't
867+ # get security-proxied.
868+ return (
869+ (date, (revision for revision in revisions))
870+ for date, revisions in gby)
871+
872
873 class BranchMergeProposalGetter:
874 """See `IBranchMergeProposalGetter`."""
875
876=== modified file 'lib/lp/code/model/directbranchcommit.py'
877--- lib/lp/code/model/directbranchcommit.py 2010-08-20 20:31:18 +0000
878+++ lib/lp/code/model/directbranchcommit.py 2010-09-22 06:47:48 +0000
879@@ -55,7 +55,8 @@
880 is_locked = False
881 commit_builder = None
882
883- def __init__(self, db_branch, committer=None, no_race_check=False):
884+ def __init__(self, db_branch, committer=None, no_race_check=False,
885+ merge_parents=None):
886 """Create context for direct commit to branch.
887
888 Before constructing a `DirectBranchCommit`, set up a server that
889@@ -107,6 +108,7 @@
890 raise
891
892 self.files = set()
893+ self.merge_parents = merge_parents
894
895 def _getDir(self, path):
896 """Get trans_id for directory "path." Create if necessary."""
897@@ -200,7 +202,8 @@
898 # required to generate the revision-id.
899 with override_environ(BZR_EMAIL=committer_id):
900 new_rev_id = self.transform_preview.commit(
901- self.bzrbranch, commit_message, committer=committer_id)
902+ self.bzrbranch, commit_message, self.merge_parents,
903+ committer=committer_id)
904 IMasterObject(self.db_branch).branchChanged(
905 get_stacked_on_url(self.bzrbranch), new_rev_id,
906 self.db_branch.control_format, self.db_branch.branch_format,
907
908=== modified file 'lib/lp/code/model/revision.py'
909--- lib/lp/code/model/revision.py 2010-08-27 02:11:36 +0000
910+++ lib/lp/code/model/revision.py 2010-09-22 06:47:48 +0000
911@@ -18,6 +18,7 @@
912 )
913 import email
914
915+from bzrlib.revision import NULL_REVISION
916 import pytz
917 from sqlobject import (
918 BoolCol,
919@@ -39,7 +40,6 @@
920 )
921 from storm.locals import (
922 Bool,
923- DateTime,
924 Int,
925 Min,
926 Reference,
927@@ -118,6 +118,13 @@
928 """
929 return [parent.parent_id for parent in self.parents]
930
931+ def getLefthandParent(self):
932+ if len(self.parent_ids) == 0:
933+ parent_id = NULL_REVISION
934+ else:
935+ parent_id = self.parent_ids[0]
936+ return RevisionSet().getByRevisionId(parent_id)
937+
938 def getProperties(self):
939 """See `IRevision`."""
940 return dict((prop.name, prop.value) for prop in self.properties)
941
942=== modified file 'lib/lp/code/model/tests/test_branch.py'
943--- lib/lp/code/model/tests/test_branch.py 2010-09-09 02:49:47 +0000
944+++ lib/lp/code/model/tests/test_branch.py 2010-09-22 06:47:48 +0000
945@@ -231,11 +231,25 @@
946 branch = self.factory.makeAnyBranch()
947 login_person(branch.owner)
948 removeSecurityProxy(branch).last_mirrored_id = 'rev1'
949- jobs = list(getUtility(IBranchScanJobSource).iterReady())
950- self.assertEqual(0, len(jobs))
951- branch.branchChanged('', 'rev1', *self.arbitrary_formats)
952- jobs = list(getUtility(IBranchScanJobSource).iterReady())
953- self.assertEqual(0, len(jobs))
954+ removeSecurityProxy(branch).last_scanned_id = 'rev1'
955+ jobs = list(getUtility(IBranchScanJobSource).iterReady())
956+ self.assertEqual(0, len(jobs))
957+ branch.branchChanged('', 'rev1', *self.arbitrary_formats)
958+ jobs = list(getUtility(IBranchScanJobSource).iterReady())
959+ self.assertEqual(0, len(jobs))
960+
961+ def test_branchChanged_creates_scan_job_for_broken_scan(self):
962+ # branchChanged() if the last_scanned_id is different to the newly
963+ # changed revision, then a scan job is created.
964+ branch = self.factory.makeAnyBranch()
965+ login_person(branch.owner)
966+ removeSecurityProxy(branch).last_mirrored_id = 'rev1'
967+ removeSecurityProxy(branch).last_scanned_id = 'old'
968+ jobs = list(getUtility(IBranchScanJobSource).iterReady())
969+ self.assertEqual(0, len(jobs))
970+ branch.branchChanged('', 'rev1', *self.arbitrary_formats)
971+ jobs = list(getUtility(IBranchScanJobSource).iterReady())
972+ self.assertEqual(1, len(jobs))
973
974 def test_branchChanged_packs_format(self):
975 # branchChanged sets the branch_format etc attributes to the passed in
976
977=== modified file 'lib/lp/code/model/tests/test_branchmergeproposal.py'
978--- lib/lp/code/model/tests/test_branchmergeproposal.py 2010-08-20 20:31:18 +0000
979+++ lib/lp/code/model/tests/test_branchmergeproposal.py 2010-09-22 06:47:48 +0000
980@@ -7,7 +7,7 @@
981
982 __metaclass__ = type
983
984-from datetime import datetime
985+from datetime import datetime, timedelta
986 from difflib import unified_diff
987 from unittest import (
988 TestCase,
989@@ -72,6 +72,7 @@
990 MergeProposalCreatedJob,
991 UpdatePreviewDiffJob,
992 )
993+from lp.code.tests.helpers import add_revision_to_branch
994 from lp.registry.interfaces.person import IPersonSet
995 from lp.registry.interfaces.product import IProductSet
996 from lp.testing import (
997@@ -108,7 +109,8 @@
998 self.assertTrue(url.startswith(source_branch_url))
999
1000 def test_BranchMergeProposal_canonical_url_rest(self):
1001- # The rest of the URL for a merge proposal is +merge followed by the db id.
1002+ # The rest of the URL for a merge proposal is +merge followed by the
1003+ # db id.
1004 bmp = self.factory.makeBranchMergeProposal()
1005 url = canonical_url(bmp)
1006 source_branch_url = canonical_url(bmp.source_branch)
1007@@ -238,7 +240,6 @@
1008 self._attemptTransition,
1009 proposal, to_state)
1010
1011-
1012 def assertGoodDupeTransition(self, from_state, to_state):
1013 """Trying to go from `from_state` to `to_state` succeeds."""
1014 proposal = self.prepareDupeTransition(from_state)
1015@@ -1049,6 +1050,7 @@
1016 else:
1017 self.assertEqual([mp], list(active))
1018
1019+
1020 class TestBranchMergeProposalGetterGetProposals(TestCaseWithFactory):
1021 """Test the getProposalsForContext method."""
1022
1023@@ -1118,7 +1120,6 @@
1024 beaver, [BranchMergeProposalStatus.REJECTED], beaver)
1025 self.assertEqual(beave_proposals.count(), 1)
1026
1027-
1028 def test_created_proposal_default_status(self):
1029 # When we create a merge proposal using the helper method, the default
1030 # status of the proposal is work in progress.
1031@@ -1799,5 +1800,67 @@
1032 self.assertIs(None, bmp.next_preview_diff_job)
1033
1034
1035+class TestRevisionEndDate(TestCaseWithFactory):
1036+
1037+ layer = DatabaseFunctionalLayer
1038+
1039+ def test_revision_end_date_active(self):
1040+ # An active merge proposal will have None as an end date.
1041+ bmp = self.factory.makeBranchMergeProposal()
1042+ self.assertIs(None, bmp.revision_end_date)
1043+
1044+ def test_revision_end_date_merged(self):
1045+ # An merged proposal will have the date merged as an end date.
1046+ bmp = self.factory.makeBranchMergeProposal(
1047+ set_state=BranchMergeProposalStatus.MERGED)
1048+ self.assertEqual(bmp.date_merged, bmp.revision_end_date)
1049+
1050+ def test_revision_end_date_rejected(self):
1051+ # An rejected proposal will have the date reviewed as an end date.
1052+ bmp = self.factory.makeBranchMergeProposal(
1053+ set_state=BranchMergeProposalStatus.REJECTED)
1054+ self.assertEqual(bmp.date_reviewed, bmp.revision_end_date)
1055+
1056+
1057+class TestGetRevisionsSinceReviewStart(TestCaseWithFactory):
1058+
1059+ layer = DatabaseFunctionalLayer
1060+
1061+ def assertRevisionGroups(self, bmp, expected_groups):
1062+ """Get the groups for the merge proposal and check them."""
1063+ groups = bmp.getRevisionsSinceReviewStart()
1064+ revision_groups = [list(revisions) for date, revisions in groups]
1065+ self.assertEqual(expected_groups, revision_groups)
1066+
1067+ def test_getRevisionsSinceReviewStart_no_revisions(self):
1068+ # If there have been no revisions pushed since the start of the
1069+ # review, the method returns an empty list.
1070+ bmp = self.factory.makeBranchMergeProposal()
1071+ self.assertRevisionGroups(bmp, [])
1072+
1073+ def test_getRevisionsSinceReviewStart_groups(self):
1074+ # Revisions that were scanned at the same time have the same
1075+ # date_created. These revisions are grouped together.
1076+ review_date = datetime(2009, 9, 10, tzinfo=UTC)
1077+ bmp = self.factory.makeBranchMergeProposal(
1078+ date_created=review_date)
1079+ login_person(bmp.registrant)
1080+ bmp.requestReview(review_date)
1081+ revision_date = review_date + timedelta(days=1)
1082+ revisions = []
1083+ for date in range(2):
1084+ revisions.append(
1085+ add_revision_to_branch(
1086+ self.factory, bmp.source_branch, revision_date))
1087+ revisions.append(
1088+ add_revision_to_branch(
1089+ self.factory, bmp.source_branch, revision_date))
1090+ revision_date += timedelta(days=1)
1091+ expected_groups = [
1092+ [revisions[0], revisions[1]],
1093+ [revisions[2], revisions[3]]]
1094+ self.assertRevisionGroups(bmp, expected_groups)
1095+
1096+
1097 def test_suite():
1098 return TestLoader().loadTestsFromName(__name__)
1099
1100=== modified file 'lib/lp/code/model/tests/test_diff.py'
1101--- lib/lp/code/model/tests/test_diff.py 2010-08-20 20:31:18 +0000
1102+++ lib/lp/code/model/tests/test_diff.py 2010-09-22 06:47:48 +0000
1103@@ -57,13 +57,14 @@
1104 class DiffTestCase(TestCaseWithFactory):
1105
1106 @staticmethod
1107- def commitFile(branch, path, contents):
1108+ def commitFile(branch, path, contents, merge_parents=None):
1109 """Create a commit that updates a file to specified contents.
1110
1111 This will create or modify the file, as needed.
1112 """
1113 committer = DirectBranchCommit(
1114- removeSecurityProxy(branch), no_race_check=True)
1115+ removeSecurityProxy(branch), no_race_check=True,
1116+ merge_parents=merge_parents)
1117 committer.writeFile(path, contents)
1118 try:
1119 return committer.commit('committing')
1120@@ -122,7 +123,6 @@
1121 prerequisite)
1122
1123
1124-
1125 class TestDiff(DiffTestCase):
1126
1127 layer = LaunchpadFunctionalLayer
1128@@ -186,19 +186,19 @@
1129 self.checkExampleMerge(diff.text)
1130
1131 diff_bytes = (
1132- "--- bar 2009-08-26 15:53:34.000000000 -0400\n"
1133- "+++ bar 1969-12-31 19:00:00.000000000 -0500\n"
1134+ "--- bar\t2009-08-26 15:53:34.000000000 -0400\n"
1135+ "+++ bar\t1969-12-31 19:00:00.000000000 -0500\n"
1136 "@@ -1,3 +0,0 @@\n"
1137 "-a\n"
1138 "-b\n"
1139 "-c\n"
1140- "--- baz 1969-12-31 19:00:00.000000000 -0500\n"
1141- "+++ baz 2009-08-26 15:53:57.000000000 -0400\n"
1142+ "--- baz\t1969-12-31 19:00:00.000000000 -0500\n"
1143+ "+++ baz\t2009-08-26 15:53:57.000000000 -0400\n"
1144 "@@ -0,0 +1,2 @@\n"
1145 "+a\n"
1146 "+b\n"
1147- "--- foo 2009-08-26 15:53:23.000000000 -0400\n"
1148- "+++ foo 2009-08-26 15:56:43.000000000 -0400\n"
1149+ "--- foo\t2009-08-26 15:53:23.000000000 -0400\n"
1150+ "+++ foo\t2009-08-26 15:56:43.000000000 -0400\n"
1151 "@@ -1,3 +1,4 @@\n"
1152 " a\n"
1153 "-b\n"
1154@@ -207,19 +207,19 @@
1155 "+e\n")
1156
1157 diff_bytes_2 = (
1158- "--- bar 2009-08-26 15:53:34.000000000 -0400\n"
1159- "+++ bar 1969-12-31 19:00:00.000000000 -0500\n"
1160+ "--- bar\t2009-08-26 15:53:34.000000000 -0400\n"
1161+ "+++ bar\t1969-12-31 19:00:00.000000000 -0500\n"
1162 "@@ -1,3 +0,0 @@\n"
1163 "-a\n"
1164 "-b\n"
1165 "-c\n"
1166- "--- baz 1969-12-31 19:00:00.000000000 -0500\n"
1167- "+++ baz 2009-08-26 15:53:57.000000000 -0400\n"
1168+ "--- baz\t1969-12-31 19:00:00.000000000 -0500\n"
1169+ "+++ baz\t2009-08-26 15:53:57.000000000 -0400\n"
1170 "@@ -0,0 +1,2 @@\n"
1171 "+a\n"
1172 "+b\n"
1173- "--- foo 2009-08-26 15:53:23.000000000 -0400\n"
1174- "+++ foo 2009-08-26 15:56:43.000000000 -0400\n"
1175+ "--- foo\t2009-08-26 15:53:23.000000000 -0400\n"
1176+ "+++ foo\t2009-08-26 15:56:43.000000000 -0400\n"
1177 "@@ -1,3 +1,5 @@\n"
1178 " a\n"
1179 "-b\n"
1180@@ -467,7 +467,6 @@
1181 self.assertEqual('', diff.conflicts)
1182 self.assertFalse(diff.has_conflicts)
1183
1184-
1185 def test_fromBranchMergeProposal(self):
1186 # Correctly generates a PreviewDiff from a BranchMergeProposal.
1187 bmp, source_rev_id, target_rev_id = self.createExampleMerge()
1188
1189=== modified file 'lib/lp/code/tests/helpers.py'
1190--- lib/lp/code/tests/helpers.py 2010-08-31 00:16:41 +0000
1191+++ lib/lp/code/tests/helpers.py 2010-09-22 06:47:48 +0000
1192@@ -64,9 +64,14 @@
1193 """
1194 if date_created is None:
1195 date_created = revision_date
1196+ parent = branch.revision_history.last()
1197+ if parent is None:
1198+ parent_ids = []
1199+ else:
1200+ parent_ids = [parent.revision.revision_id]
1201 revision = factory.makeRevision(
1202 revision_date=revision_date, date_created=date_created,
1203- log_body=commit_msg)
1204+ log_body=commit_msg, parent_ids=parent_ids)
1205 if mainline:
1206 sequence = branch.revision_count + 1
1207 branch_revision = branch.createBranchRevision(sequence, revision)
1208@@ -112,7 +117,7 @@
1209 preview.remvoed_lines_count = 13
1210 preview.diffstat = {'file1': (3, 8), 'file2': (4, 5)}
1211 return {
1212- 'eric': eric, 'fooix': fooix, 'trunk':trunk, 'feature': feature,
1213+ 'eric': eric, 'fooix': fooix, 'trunk': trunk, 'feature': feature,
1214 'proposed': proposed, 'fred': fred}
1215
1216
1217
1218=== modified file 'lib/lp/code/tests/test_directbranchcommit.py'
1219--- lib/lp/code/tests/test_directbranchcommit.py 2010-08-20 20:31:18 +0000
1220+++ lib/lp/code/tests/test_directbranchcommit.py 2010-09-22 06:47:48 +0000
1221@@ -99,6 +99,24 @@
1222 branch_revision_id = self.committer.bzrbranch.last_revision()
1223 self.assertEqual(branch_revision_id, revision_id)
1224
1225+ def test_commit_uses_merge_parents(self):
1226+ # DirectBranchCommit.commit returns uses merge parents
1227+ self._tearDownCommitter()
1228+ # Merge parents cannot be specified for initial commit, so do an
1229+ # empty commit.
1230+ self.tree.commit('foo', committer='foo@bar', rev_id='foo')
1231+ committer = DirectBranchCommit(
1232+ self.db_branch, merge_parents=['parent-1', 'parent-2'])
1233+ committer.last_scanned_id = (
1234+ committer.bzrbranch.last_revision())
1235+ committer.writeFile('file.txt', 'contents')
1236+ revision_id = committer.commit('')
1237+ branch_revision_id = committer.bzrbranch.last_revision()
1238+ branch_revision = committer.bzrbranch.repository.get_revision(
1239+ branch_revision_id)
1240+ self.assertEqual(
1241+ ['parent-1', 'parent-2'], branch_revision.parent_ids[1:])
1242+
1243 def test_DirectBranchCommit_aborts_cleanly(self):
1244 # If a DirectBranchCommit is not committed, its changes do not
1245 # go into the branch.
1246
1247=== modified file 'lib/lp/codehosting/branchdistro.py'
1248--- lib/lp/codehosting/branchdistro.py 2010-08-20 20:31:18 +0000
1249+++ lib/lp/codehosting/branchdistro.py 2010-09-22 06:47:48 +0000
1250@@ -28,7 +28,7 @@
1251
1252 from canonical.config import config
1253 from canonical.launchpad.interfaces import ILaunchpadCelebrities
1254-from lp.code.enums import BranchType
1255+from lp.code.enums import BranchLifecycleStatus, BranchType
1256 from lp.code.errors import BranchExists
1257 from lp.code.interfaces.branchcollection import IAllBranches
1258 from lp.code.interfaces.branchnamespace import IBranchNamespaceSet
1259@@ -341,6 +341,7 @@
1260 new_db_branch.sourcepackage.setBranch(
1261 PackagePublishingPocket.RELEASE, new_db_branch,
1262 getUtility(ILaunchpadCelebrities).ubuntu_branches.teamowner)
1263+ old_db_branch.lifecycle_status = BranchLifecycleStatus.MATURE
1264 # switch_branches *moves* the data to locations dependent on the
1265 # new_branch's id, so if the transaction was rolled back we wouldn't
1266 # know the branch id and thus wouldn't be able to find the branch data
1267
1268=== modified file 'lib/lp/codehosting/bzrutils.py'
1269--- lib/lp/codehosting/bzrutils.py 2010-08-20 20:31:18 +0000
1270+++ lib/lp/codehosting/bzrutils.py 2010-09-22 06:47:48 +0000
1271@@ -18,11 +18,13 @@
1272 'identical_formats',
1273 'install_oops_handler',
1274 'is_branch_stackable',
1275+ 'read_locked',
1276 'remove_exception_logging_hook',
1277 'safe_open',
1278 'UnsafeUrlSeen',
1279 ]
1280
1281+from contextlib import contextmanager
1282 import os
1283 import sys
1284 import threading
1285@@ -363,3 +365,12 @@
1286 return branch.get_stacked_on_url()
1287 except (NotStacked, UnstackableBranchFormat):
1288 return None
1289+
1290+
1291+@contextmanager
1292+def read_locked(branch):
1293+ branch.lock_read()
1294+ try:
1295+ yield
1296+ finally:
1297+ branch.unlock()
1298
1299=== modified file 'lib/lp/codehosting/tests/test_branchdistro.py'
1300--- lib/lp/codehosting/tests/test_branchdistro.py 2010-08-20 20:31:18 +0000
1301+++ lib/lp/codehosting/tests/test_branchdistro.py 2010-09-22 06:47:48 +0000
1302@@ -34,6 +34,7 @@
1303 QuietFakeLogger,
1304 )
1305 from canonical.testing.layers import LaunchpadZopelessLayer
1306+from lp.code.enums import BranchLifecycleStatus
1307 from lp.codehosting.branchdistro import (
1308 DistroBrancher,
1309 switch_branches,
1310@@ -233,6 +234,12 @@
1311 db_branch.sourcepackagename, brancher.new_distroseries.name],
1312 [new_branch.owner, new_branch.distribution,
1313 new_branch.sourcepackagename, new_branch.name])
1314+ # The new branch is set in the development state, and the old one is
1315+ # mature.
1316+ self.assertEqual(
1317+ BranchLifecycleStatus.DEVELOPMENT, new_branch.lifecycle_status)
1318+ self.assertEqual(
1319+ BranchLifecycleStatus.MATURE, db_branch.lifecycle_status)
1320
1321 def test_makeOneNewBranch_inconsistent_branch(self):
1322 # makeOneNewBranch skips over an inconsistent official package branch
1323
1324=== modified file 'lib/lp/registry/browser/__init__.py'
1325--- lib/lp/registry/browser/__init__.py 2010-09-03 15:02:39 +0000
1326+++ lib/lp/registry/browser/__init__.py 2010-09-22 06:47:48 +0000
1327@@ -7,7 +7,6 @@
1328
1329 __all__ = [
1330 'get_status_counts',
1331- 'MapMixin',
1332 'MilestoneOverlayMixin',
1333 'RegistryEditFormView',
1334 'RegistryDeleteViewMixin',
1335@@ -32,7 +31,6 @@
1336 )
1337 from lp.registry.interfaces.productseries import IProductSeries
1338 from lp.registry.interfaces.series import SeriesStatus
1339-from lp.services.propertycache import cachedproperty
1340
1341
1342 class StatusCount:
1343@@ -258,19 +256,3 @@
1344 @action("Change", name='change')
1345 def change_action(self, action, data):
1346 self.updateContextFromData(data)
1347-
1348-
1349-class MapMixin:
1350-
1351- @cachedproperty
1352- def gmap2_enabled(self):
1353- # XXX sinzui 2010-08-27 bug=625556: This is a hack to use
1354- # feature flags, which are not ready for general use in the production
1355- # code, but has just enough to support this use case:
1356- # Do not enable gmap2 if Google's service is not operational.
1357- from lp.services.features.flags import FeatureController
1358-
1359- def in_scope(value):
1360- return True
1361-
1362- return FeatureController(in_scope).getFlag('gmap2') == 'on'
1363
1364=== modified file 'lib/lp/registry/browser/configure.zcml'
1365--- lib/lp/registry/browser/configure.zcml 2010-09-13 10:04:20 +0000
1366+++ lib/lp/registry/browser/configure.zcml 2010-09-22 06:47:48 +0000
1367@@ -829,7 +829,7 @@
1368 for="lp.registry.interfaces.person.IPerson"
1369 class="lp.registry.browser.person.PersonEditLocationView"
1370 permission="launchpad.Edit"
1371- template="../templates/person-editlocation.pt"/>
1372+ template="../../app/templates/generic-edit.pt"/>
1373 <browser:page
1374 name="+contactuser"
1375 for="lp.registry.interfaces.person.IPerson"
1376@@ -1080,11 +1080,6 @@
1377 <browser:page
1378 for="lp.registry.interfaces.person.ITeam"
1379 permission="zope.Public"
1380- class="lp.registry.browser.person.TeamEditLocationView"
1381- name="+editlocation"/>
1382- <browser:page
1383- for="lp.registry.interfaces.person.ITeam"
1384- permission="zope.Public"
1385 name="+listing-simple"
1386 template="../templates/team-listing-simple.pt"/>
1387 <browser:page
1388
1389=== modified file 'lib/lp/registry/browser/person.py'
1390--- lib/lp/registry/browser/person.py 2010-09-19 00:35:22 +0000
1391+++ lib/lp/registry/browser/person.py 2010-09-22 06:47:48 +0000
1392@@ -65,7 +65,6 @@
1393 'SearchSubscribedQuestionsView',
1394 'TeamAddMyTeamsView',
1395 'TeamBreadcrumb',
1396- 'TeamEditLocationView',
1397 'TeamEditMenu',
1398 'TeamIndexMenu',
1399 'TeamJoinView',
1400@@ -148,7 +147,6 @@
1401 helpers,
1402 )
1403 from canonical.launchpad.browser.feeds import FeedsMixin
1404-from canonical.launchpad.browser.launchpad import get_launchpad_views
1405 from canonical.launchpad.interfaces.account import (
1406 AccountStatus,
1407 IAccount,
1408@@ -244,7 +242,6 @@
1409 from lp.code.browser.sourcepackagerecipelisting import HasRecipesMenuMixin
1410 from lp.code.errors import InvalidNamespace
1411 from lp.code.interfaces.branchnamespace import IBranchNamespaceSet
1412-from lp.registry.browser import MapMixin
1413 from lp.registry.browser.branding import BrandingChangeView
1414 from lp.registry.browser.mailinglists import enabled_with_active_mailing_list
1415 from lp.registry.browser.menu import (
1416@@ -3325,7 +3322,7 @@
1417 return self.state is EmailAddressVisibleState.ALLOWED
1418
1419
1420-class PersonIndexView(XRDSContentNegotiationMixin, MapMixin, PersonView):
1421+class PersonIndexView(XRDSContentNegotiationMixin, PersonView):
1422 """View class for person +index and +xrds pages."""
1423
1424 xrds_template = ViewPageTemplateFile(
1425@@ -3333,14 +3330,6 @@
1426
1427 def initialize(self):
1428 super(PersonIndexView, self).initialize()
1429- # This view requires the gmap2 Javascript in order to render the map
1430- # with the person's usual location. The location is only availble if
1431- # the location is set, visible, and the viewing user wants to see it.
1432- launchpad_views = get_launchpad_views(self.request.cookies)
1433- self._small_map = launchpad_views['small_maps']
1434- if (self.gmap2_enabled
1435- and self.has_visible_location and self._small_map):
1436- self.request.needs_gmap2 = True
1437 if self.request.method == "POST":
1438 self.processForm()
1439
1440@@ -3397,9 +3386,6 @@
1441 assert self.has_visible_location, (
1442 "Can't generate the map for a person who hasn't set a "
1443 "visible location.")
1444- assert self.request.needs_gmap2 or not self._small_map, (
1445- "To use this method a view must flag that it needs gmap2.")
1446-
1447 replacements = {'center_lat': self.context.latitude,
1448 'center_lng': self.context.longitude}
1449 return u"""
1450@@ -5638,7 +5624,7 @@
1451 class PersonLocationForm(Interface):
1452
1453 location = LocationField(
1454- title=_('Use the map to indicate default location'),
1455+ title=_('Time zone'),
1456 required=True)
1457 hide = Bool(
1458 title=_("Hide my location details from others."),
1459@@ -5649,37 +5635,15 @@
1460 """Edit a person's location."""
1461
1462 schema = PersonLocationForm
1463- field_names = ['location', 'hide']
1464+ field_names = ['location']
1465 custom_widget('location', LocationWidget)
1466-
1467- @property
1468- def page_title(self):
1469- return smartquote(
1470- "%s's location and timezone" % self.context.displayname)
1471-
1472- label = page_title
1473-
1474- @property
1475- def initial_values(self):
1476- """See `LaunchpadFormView`.
1477-
1478- Set the initial value for the 'hide' field. The initial value for the
1479- 'location' field is set by its widget.
1480- """
1481- if self.context.location is None:
1482- return {}
1483- else:
1484- return {'hide': not self.context.location.visible}
1485-
1486- def initialize(self):
1487- self.next_url = canonical_url(self.context)
1488- self.for_team_name = self.request.form.get('for_team')
1489- if self.for_team_name is not None:
1490- for_team = getUtility(IPersonSet).getByName(self.for_team_name)
1491- if for_team is not None:
1492- self.next_url = canonical_url(for_team) + '/+map'
1493- super(PersonEditLocationView, self).initialize()
1494- self.cancel_url = self.next_url
1495+ page_title = label = 'Set timezone'
1496+
1497+ @property
1498+ def next_url(self):
1499+ return canonical_url(self.context)
1500+
1501+ cancel_url = next_url
1502
1503 @action(_("Update"), name="update")
1504 def action_update(self, action, data):
1505@@ -5696,17 +5660,6 @@
1506 self.context.setLocationVisibility(visible)
1507
1508
1509-class TeamEditLocationView(LaunchpadView):
1510- """Redirect to the team's +map page.
1511-
1512- We do that because it doesn't make sense to specify the location of a
1513- team."""
1514-
1515- def initialize(self):
1516- self.request.response.redirect(
1517- canonical_url(self.context, view_name="+map"))
1518-
1519-
1520 def archive_to_person(archive):
1521 """Adapts an `IArchive` to an `IPerson`."""
1522 return IPerson(archive.owner)
1523
1524=== modified file 'lib/lp/registry/browser/team.py'
1525--- lib/lp/registry/browser/team.py 2010-09-19 03:13:01 +0000
1526+++ lib/lp/registry/browser/team.py 2010-09-22 06:47:48 +0000
1527@@ -25,10 +25,7 @@
1528
1529 from datetime import datetime
1530 import math
1531-from urllib import (
1532- quote,
1533- unquote,
1534- )
1535+from urllib import unquote
1536
1537 import pytz
1538 from zope.app.form.browser import TextAreaWidget
1539@@ -71,7 +68,6 @@
1540 LaunchpadRadioWidget,
1541 )
1542 from lp.app.errors import UnexpectedFormData
1543-from lp.registry.browser import MapMixin
1544 from lp.registry.browser.branding import BrandingChangeView
1545 from lp.registry.interfaces.mailinglist import (
1546 IMailingList,
1547@@ -1083,7 +1079,7 @@
1548 self.request.response.addInfoNotification(msg)
1549
1550
1551-class TeamMapView(MapMixin, LaunchpadView):
1552+class TeamMapView(LaunchpadView):
1553 """Show all people with known locations on a map.
1554
1555 Also provides links to edit the locations of people in the team without
1556@@ -1093,12 +1089,6 @@
1557 label = "Team member locations"
1558 limit = None
1559
1560- def initialize(self):
1561- # Tell our base-layout to include Google's gmap2 javascript so that
1562- # we can render the map.
1563- if self.gmap2_enabled and self.mapped_participants_count > 0:
1564- self.request.needs_gmap2 = True
1565-
1566 @cachedproperty
1567 def mapped_participants(self):
1568 """Participants with locations."""
1569
1570=== modified file 'lib/lp/registry/browser/tests/person-views.txt'
1571--- lib/lp/registry/browser/tests/person-views.txt 2010-08-28 23:01:18 +0000
1572+++ lib/lp/registry/browser/tests/person-views.txt 2010-09-22 06:47:48 +0000
1573@@ -301,113 +301,6 @@
1574 Portuguese (Brazil)
1575
1576
1577-Location
1578---------
1579-
1580-The Person profile page contains the location portlet that shows a map.
1581-The map requires the google GMap JavaScript to display, so the views set
1582-the state of the request's needs_gmap2 attribute to True only when the
1583-user has set his latitude, it is visible, and the viewing user wishes to
1584-see it. The map is not rendered if the user has not set his location.
1585-
1586- >>> sample_person.latitude is None
1587- True
1588-
1589- >>> person_view = create_initialized_view(sample_person, '+index')
1590- >>> person_view.request.needs_gmap2
1591- False
1592-
1593-The map_portlet_html property that creates the map cannot be called.
1594-
1595- >>> print person_view.map_portlet_html
1596- Traceback (most recent call last):
1597- ...
1598- AssertionError: Can't generate the map for a person who hasn't set
1599- a visible location.
1600-
1601-If the user sets his location, but does not make it visible, needs_gmap2
1602-will still be False and the map_portlet_html property cannot be called.
1603-
1604- >>> login_person(sample_person)
1605- >>> sample_person.setLocation(
1606- ... 38.81, 77.1, 'America/New_York', sample_person)
1607- >>> sample_person.setLocationVisibility(False)
1608- >>> login('no-priv@canonical.com')
1609-
1610- >>> person_view = create_initialized_view(sample_person, '+index')
1611- >>> person_view.request.needs_gmap2
1612- False
1613-
1614- >>> print person_view.map_portlet_html
1615- Traceback (most recent call last):
1616- ...
1617- AssertionError: Can't generate the map for a person who hasn't set
1618- a visible location.
1619-
1620-When the user set's his visibility to True, needs_gmap2 will be true and
1621-the map_portlet_html can be called.
1622-
1623- >>> from lp.services.features.model import FeatureFlag, getFeatureStore
1624- >>> ignore = getFeatureStore().add(FeatureFlag(
1625- ... scope=u'default', flag=u'gmap2', value=u'on', priority=1))
1626- >>> transaction.commit()
1627-
1628- >>> login_person(sample_person)
1629- >>> sample_person.setLocationVisibility(True)
1630-
1631- >>> person_view = create_initialized_view(sample_person, '+index')
1632- >>> person_view.request.needs_gmap2
1633- True
1634-
1635- >>> print person_view.map_portlet_html
1636- <script type="text/javascript">
1637- YUI().use('node', 'lp.app.mapping', function(Y) { ...
1638-
1639-The small_maps key in the launchpad_views cookie can be set of the
1640-viewing user to 'false' to indicate that small maps are not wanted.
1641-While needs_gmap2 is False, the map_portlet_html property's markup is
1642-still needed to render the 'Show maps' checkbox.
1643-
1644- >>> cookie = 'launchpad_views=small_maps=false'
1645- >>> person_view = create_initialized_view(
1646- ... sample_person, '+index', cookie=cookie)
1647- >>> person_view.request.needs_gmap2
1648- False
1649-
1650- >>> print person_view.map_portlet_html
1651- <script type="text/javascript">
1652- YUI().use('node', 'lp.app.mapping', function(Y) { ...
1653-
1654-The map portlet is shown if the user has not set his location and is
1655-viewing his own page.
1656-
1657- >>> user = factory.makePerson()
1658- >>> user.latitude is None
1659- True
1660-
1661- >>> login_person(user)
1662- >>> person_view = create_initialized_view(
1663- ... user, '+index')
1664- >>> person_view.should_show_map_portlet
1665- True
1666-
1667-However another user will not be shown the portlet.
1668-
1669- >>> login('foo.bar@canonical.com')
1670- >>> person_view = create_initialized_view(
1671- ... user, '+index')
1672- >>> person_view.should_show_map_portlet
1673- False
1674-
1675-If a user has a location set and it is visibible then the portlet is
1676-shown.
1677-
1678- >>> person_view = create_initialized_view(
1679- ... sample_person, '+index')
1680- >>> person_view.should_show_map_portlet
1681- True
1682-
1683-
1684 Things a person is working on
1685 -----------------------------
1686
1687
1688=== modified file 'lib/lp/registry/browser/tests/team-views.txt'
1689--- lib/lp/registry/browser/tests/team-views.txt 2010-08-28 23:01:18 +0000
1690+++ lib/lp/registry/browser/tests/team-views.txt 2010-09-22 06:47:48 +0000
1691@@ -67,21 +67,11 @@
1692
1693 == +map-portlet ==
1694
1695-The team profile page contain the location portlet that shows a map. The
1696-map requires the google GMap JavaScript to display, so the views set the
1697-state of the request's needs_gmap2 attribute to true if there are
1698-members who have set their location.
1699-
1700- >>> from lp.services.features.model import FeatureFlag, getFeatureStore
1701- >>> ignore = getFeatureStore().add(FeatureFlag(
1702- ... scope=u'default', flag=u'gmap2', value=u'on', priority=1))
1703- >>> transaction.commit()
1704+The team profile page contain the location portlet that shows a map.
1705
1706 >>> team_view = create_initialized_view(ubuntu_team, '+index')
1707 >>> team_view.has_visible_location
1708 False
1709- >>> team_view.request.needs_gmap2
1710- False
1711
1712 After a member has set his location, the map will be rendered.
1713
1714@@ -92,8 +82,6 @@
1715 >>> team_view = create_initialized_view(ubuntu_team, '+index')
1716 >>> team_view.has_visible_location
1717 True
1718- >>> team_view.request.needs_gmap2
1719- True
1720
1721 The small_maps key in the launchpad_views cookie can be set by the viewing
1722 user to 'false' to indicate that small maps are not wanted.
1723@@ -101,9 +89,6 @@
1724 >>> cookie = 'launchpad_views=small_maps=false'
1725 >>> team_view = create_initialized_view(
1726 ... ubuntu_team, '+index', cookie=cookie)
1727- >>> team_view.request.needs_gmap2
1728- False
1729-
1730
1731 == +map ==
1732
1733@@ -118,15 +103,10 @@
1734 >>> view.times
1735 []
1736
1737-There are no mapped member yet, so needs_gmap2 is False, so the map will
1738-not be rendered.
1739-
1740- >>> view.request.needs_gmap2
1741- False
1742-
1743-Once a member is mapped, needs_gmap2 is True and the map will be rendered.
1744-The view provides a cached property to access the mapped participants. The
1745-views number of times is incremented for each timezone the members reside in.
1746+
1747+Once a member is mapped, the map will be rendered. The view provides a cached
1748+property to access the mapped participants. The views number of times is
1749+incremented for each timezone the members reside in.
1750
1751 >>> london_member = factory.makePerson(
1752 ... latitude=51.49, longitude=-0.13, time_zone='Europe/London')
1753@@ -139,9 +119,6 @@
1754 >>> len(view.times)
1755 1
1756
1757- >>> view.request.needs_gmap2
1758- True
1759-
1760 >>> brazil_member = factory.makePerson(
1761 ... latitude=-23.60, longitude=-46.64, time_zone='America/Sao_Paulo')
1762 >>> ignored = context.addMember(brazil_member, mark)
1763
1764=== modified file 'lib/lp/registry/doc/personlocation.txt'
1765--- lib/lp/registry/doc/personlocation.txt 2010-08-20 12:25:28 +0000
1766+++ lib/lp/registry/doc/personlocation.txt 2010-09-22 06:47:48 +0000
1767@@ -1,4 +1,5 @@
1768-= Locations for People and Teams =
1769+Locations for People and Teams
1770+==============================
1771
1772 The PersonLocation object stores information about the location and time
1773 zone of a person. It also remembers who provided that information, and
1774@@ -164,7 +165,8 @@
1775 mapped_participants_count == 0.
1776
1777
1778-== Location visibility ==
1779+Location visibility
1780+-------------------
1781
1782 Some people may not want their location to be disclosed to others, so
1783 we provide a way for them to hide their location from other users. By
1784
1785=== modified file 'lib/lp/registry/stories/location/personlocation-edit.txt'
1786--- lib/lp/registry/stories/location/personlocation-edit.txt 2009-11-15 01:05:49 +0000
1787+++ lib/lp/registry/stories/location/personlocation-edit.txt 2010-09-22 06:47:48 +0000
1788@@ -1,4 +1,5 @@
1789-== Edit person location information ==
1790+Edit person location information
1791+================================
1792
1793 A person's location is only editable by people who have launchpad.Edit on
1794 the person, which is that person and admins.
1795@@ -49,35 +50,3 @@
1796 >>> admin_browser.open('http://launchpad.dev/~zzz/+editlocation')
1797 >>> admin_browser.getControl(name='field.location.latitude').value
1798 '39.48'
1799-
1800-The +editlocation page also allows a person to change his location
1801-visibility, that is, whether or not others can see it.
1802-
1803- >>> nopriv_browser.open('http://launchpad.dev/~no-priv/+editlocation')
1804- >>> nopriv_browser.getControl(
1805- ... 'Hide my location details from others.').selected = True
1806- >>> nopriv_browser.getControl('Update').click()
1807- >>> nopriv_browser.url
1808- 'http://launchpad.dev/~no-priv'
1809-
1810-Once hidden, other users can't see it.
1811-
1812- >>> name12_browser = setupBrowser(auth="Basic test@canonical.com:test")
1813- >>> name12_browser.open('http://launchpad.dev/~no-priv')
1814- >>> print str(find_tag_by_id(name12_browser.contents, 'person_map_div'))
1815- None
1816-
1817-The person himself can still see and change it, though.
1818-
1819- >>> nopriv_browser.open('http://launchpad.dev/~no-priv')
1820- >>> print str(find_tag_by_id(nopriv_browser.contents, 'portlet-map'))
1821- <div...
1822- <h2>Location</h2>
1823- ...
1824-
1825- >>> nopriv_browser.open('http://launchpad.dev/~no-priv/+editlocation')
1826- >>> nopriv_browser.getControl(
1827- ... 'Hide my location details from others.').selected = False
1828- >>> nopriv_browser.getControl('Update').click()
1829- >>> nopriv_browser.url
1830- 'http://launchpad.dev/~no-priv'
1831
1832=== modified file 'lib/lp/registry/stories/location/personlocation.txt'
1833--- lib/lp/registry/stories/location/personlocation.txt 2010-08-27 22:42:17 +0000
1834+++ lib/lp/registry/stories/location/personlocation.txt 2010-09-22 06:47:48 +0000
1835@@ -16,43 +16,3 @@
1836 >>> anon_browser.open('http://launchpad.dev/~zzz')
1837 >>> print extract_text(
1838 ... find_tag_by_id(anon_browser.contents, 'portlet-map'))
1839-
1840-If a person has a location, but the gmap2 feature is not enabled, the user
1841-sees the timezone, but no map.
1842-
1843- >>> login('test@canonical.com')
1844- >>> yyy = factory.makePerson(name='yyy', time_zone='Europe/London',
1845- ... latitude=52.2, longitude=0.3)
1846- >>> logout()
1847-
1848- >>> anon_browser.open('http://launchpad.dev/~yyy')
1849- >>> markup = str(anon_browser.contents)
1850- >>> print extract_text(
1851- ... find_tag_by_id(markup, 'portlet-map'), skip_tags=[])
1852- Location
1853- Time zone: Europe/London...
1854-
1855- >>> 'src="http://maps.google.com/maps' in markup
1856- False
1857-
1858-If a person has a location, there is a little map portlet in their
1859-profile page. We can't test all the google javascript, but we can make sure
1860-there's a map, and the scripts are loaded when the gmap2 feature is enabled
1861-for users.
1862-
1863- >>> from lp.services.features.model import FeatureFlag, getFeatureStore
1864- >>> ignore = getFeatureStore().add(FeatureFlag(
1865- ... scope=u'default', flag=u'gmap2', value=u'on', priority=1))
1866- >>> transaction.commit()
1867-
1868- >>> anon_browser.open('http://launchpad.dev/~yyy')
1869- >>> markup = str(anon_browser.contents)
1870- >>> print extract_text(
1871- ... find_tag_by_id(markup, 'portlet-map'), skip_tags=[])
1872- Location
1873- Time zone: Europe/London...
1874- Y.lp.app.mapping.renderPersonMapSmall(...
1875- >>> 'src="http://maps.google.com/maps' in markup
1876- True
1877- >>> 'build/app/mapping.js' in markup
1878- True
1879
1880=== modified file 'lib/lp/registry/stories/location/team-map.txt'
1881--- lib/lp/registry/stories/location/team-map.txt 2010-08-27 22:33:36 +0000
1882+++ lib/lp/registry/stories/location/team-map.txt 2010-09-22 06:47:48 +0000
1883@@ -1,50 +1,6 @@
1884 The map of a team's members
1885 ===========================
1886
1887-Maps are disabled
1888------------------
1889-
1890-Users cannot see maps when the gmap2 feature is disbaled for them
1891-
1892- >>> user_browser.open('http://launchpad.dev/~guadamen')
1893- >>> body = find_main_content(user_browser.contents)
1894- >>> mapdiv = find_tag_by_id(str(body), 'team_map_div')
1895- >>> 'lp.app.mapping.renderTeamMapSmall(' in str(body)
1896- False
1897-
1898-
1899-Maps are enabled
1900-----------------
1901-
1902-Users can see maps when the gmap2 feature is enabled for them.
1903-
1904- >>> from lp.services.features.model import FeatureFlag, getFeatureStore
1905- >>> ignore = getFeatureStore().add(FeatureFlag(
1906- ... scope=u'default', flag=u'gmap2', value=u'on', priority=1))
1907- >>> transaction.commit()
1908-
1909-If a team has members that have locations, then you should see a portlet
1910-with their locations displayed.
1911-
1912- >>> nopriv_browser = setupBrowser(auth='Basic no-priv@canonical.com:test')
1913- >>> nopriv_browser.open('http://launchpad.dev/~guadamen')
1914- >>> body = find_main_content(nopriv_browser.contents)
1915- >>> mapdiv = find_tag_by_id(str(body), 'team_map_div')
1916- >>> 'lp.app.mapping.renderTeamMapSmall(' in str(body)
1917- True
1918- >>> markup = str(nopriv_browser.contents)
1919- >>> 'src="http://maps.google.com/maps' in markup
1920- True
1921- >>> 'build/app/mapping.js' in markup
1922- True
1923-
1924-You should also be able to see a map of the team.
1925-
1926- >>> maplink = nopriv_browser.getLink('View map and time zones')
1927- >>> maplink.click()
1928- >>> nopriv_browser.url
1929- 'http://launchpad.dev/~guadamen/+map'
1930-
1931 The map depends on a stream of XML-formatted data, giving the locations of
1932 all members of the team. We show that this stream works for teams with, and
1933 without, mapped members.
1934@@ -108,15 +64,6 @@
1935 <BLANKLINE>
1936
1937
1938-It doesn't make sense to edit the location of the team itself, not even
1939-if we are an admin, so a team's +editlocation page will simply redirect
1940-to +map.
1941-
1942- >>> admin_browser.open('http://launchpad.dev/~guadamen/+editlocation')
1943- >>> print admin_browser.url
1944- http://launchpad.dev/~guadamen/+map
1945-
1946-
1947 +mapdata
1948 --------
1949
1950
1951=== removed file 'lib/lp/registry/templates/person-editlocation.pt'
1952--- lib/lp/registry/templates/person-editlocation.pt 2009-09-01 19:34:46 +0000
1953+++ lib/lp/registry/templates/person-editlocation.pt 1970-01-01 00:00:00 +0000
1954@@ -1,23 +0,0 @@
1955-<html
1956- xmlns="http://www.w3.org/1999/xhtml"
1957- xmlns:tal="http://xml.zope.org/namespaces/tal"
1958- xmlns:metal="http://xml.zope.org/namespaces/metal"
1959- xmlns:i18n="http://xml.zope.org/namespaces/i18n"
1960- metal:use-macro="view/macro:page/main_only"
1961- i18n:domain="launchpad">
1962-
1963-<body>
1964-
1965- <div metal:fill-slot="main">
1966-
1967- <div metal:use-macro="context/@@launchpad_form/form">
1968- <div metal:fill-slot="extra_info">
1969- <input type="hidden" name="for_team" value=""
1970- tal:attributes="value view/for_team_name" />
1971- </div>
1972- </div>
1973-
1974- </div>
1975-
1976-</body>
1977-</html>
1978
1979=== modified file 'lib/lp/registry/templates/person-portlet-map.pt'
1980--- lib/lp/registry/templates/person-portlet-map.pt 2010-08-27 22:33:36 +0000
1981+++ lib/lp/registry/templates/person-portlet-map.pt 2010-09-22 06:47:48 +0000
1982@@ -8,7 +8,6 @@
1983 tal:define="overview_menu context/menu:overview">
1984
1985 <tal:show-map condition="view/should_show_map_portlet">
1986-
1987 <h2>Location</h2>
1988
1989 <div tal:condition="context/time_zone">
1990@@ -16,30 +15,6 @@
1991 <span tal:replace="context/time_zone">UTC</span>
1992 <a tal:replace="structure overview_menu/editlocation/fmt:icon" />
1993 </div>
1994-
1995-
1996- <tal:gmap2 condition="view/gmap2_enabled">
1997- <div style="width: 400px;" tal:condition="context/latitude">
1998- <div id="person_map_actions"
1999- style="position:relative; z-index: 9999;
2000- float:right; width: 8.5em; margin: 2px;
2001- background-color: white; padding-bottom:1px;"></div>
2002- <div id="person_map_div" class="small-map"
2003- style="height: 200px; border: 1px; margin-top: 4px;"></div>
2004- <tal:mapscript replace="structure view/map_portlet_html" />
2005- <div style="margin-top: 0px;">
2006- <a tal:replace="structure overview_menu/editlocation/fmt:link-icon" />
2007- </div>
2008- </div>
2009- </tal:gmap2>
2010-
2011- <tal:comment condition="nothing">
2012- Only the user can see the editlocation image and link.
2013- </tal:comment>
2014- <a tal:condition="not: context/latitude"
2015- tal:attributes="href overview_menu/editlocation/target"
2016- ><img src="/+icing/portlet-map-unknown.png" />
2017- </a>
2018 </tal:show-map>
2019
2020 </div>
2021
2022=== modified file 'lib/lp/registry/templates/team-index.pt'
2023--- lib/lp/registry/templates/team-index.pt 2010-06-28 21:56:49 +0000
2024+++ lib/lp/registry/templates/team-index.pt 2010-09-22 06:47:48 +0000
2025@@ -88,9 +88,6 @@
2026 <metal:subteam-of use-macro="context/@@+person-macros/subteam-of" />
2027 </div>
2028 </div>
2029-
2030- <div tal:content="structure context/@@+portlet-map" />
2031-
2032 </div>
2033 </body>
2034 </html>
2035
2036=== modified file 'lib/lp/registry/templates/team-portlet-map.pt'
2037--- lib/lp/registry/templates/team-portlet-map.pt 2010-08-27 22:33:36 +0000
2038+++ lib/lp/registry/templates/team-portlet-map.pt 2010-09-22 06:47:48 +0000
2039@@ -5,8 +5,7 @@
2040 omit-tag="">
2041
2042 <div class="portlet" id="portlet-map" style="margin-bottom: 0px;"
2043- tal:define="link context/menu:overview/map"
2044- tal:condition="view/gmap2_enabled">
2045+ tal:define="link context/menu:overview/map">
2046 <table>
2047 <tr><td>
2048 <h2>
2049
2050=== modified file 'test_on_merge.py'
2051--- test_on_merge.py 2010-08-10 21:27:56 +0000
2052+++ test_on_merge.py 2010-09-22 06:47:48 +0000
2053@@ -1,6 +1,6 @@
2054 #!/usr/bin/python -S
2055 #
2056-# Copyright 2009 Canonical Ltd. This software is licensed under the
2057+# Copyright 2009, 2010 Canonical Ltd. This software is licensed under the
2058 # GNU Affero General Public License version 3 (see the file LICENSE).
2059
2060 """Tests that get run automatically on a merge."""
2061@@ -13,7 +13,7 @@
2062 import psycopg2
2063 from subprocess import Popen, PIPE, STDOUT
2064 from signal import SIGKILL, SIGTERM, SIGINT, SIGHUP
2065-from select import select
2066+import select
2067
2068
2069 # The TIMEOUT setting (expressed in seconds) affects how long a test will run
2070@@ -164,7 +164,22 @@
2071 # Popen.communicate() with large data sets.
2072 open_readers = set([xvfb_proc.stdout])
2073 while open_readers:
2074- rlist, wlist, xlist = select(open_readers, [], [], TIMEOUT)
2075+ # blocks for a long time and can easily fail with EINTR
2076+ # <https://bugs.launchpad.net/launchpad/+bug/615740> - catching
2077+ # it just here is not the perfect fix (other syscalls might be
2078+ # interrupted) but is pragmatic
2079+ while True:
2080+ try:
2081+ rlist, wlist, xlist = select.select(open_readers, [], [], TIMEOUT)
2082+ break
2083+ except select.error, e:
2084+ # nb: select.error doesn't expose a named 'errno' attribute,
2085+ # at least in python 2.6.5; see
2086+ # <http://mail.python.org/pipermail/python-dev/2000-October/009671.html>
2087+ if e[0] == errno.EINTR:
2088+ continue
2089+ else:
2090+ raise
2091
2092 if len(rlist) == 0:
2093 # The select() statement timed out!

Subscribers

People subscribed via source and target branches

to status/vote changes: