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
=== modified file 'database/schema/security.cfg'
--- database/schema/security.cfg 2010-09-20 23:40:08 +0000
+++ database/schema/security.cfg 2010-09-22 06:47:48 +0000
@@ -629,7 +629,7 @@
629629
630[branch-distro]630[branch-distro]
631type=user631type=user
632public.branch = SELECT, INSERT632public.branch = SELECT, INSERT, UPDATE
633public.branchsubscription = SELECT, INSERT633public.branchsubscription = SELECT, INSERT
634public.distribution = SELECT634public.distribution = SELECT
635public.distroseries = SELECT635public.distroseries = SELECT
636636
=== modified file 'lib/canonical/launchpad/doc/location-widget.txt'
--- lib/canonical/launchpad/doc/location-widget.txt 2010-08-11 15:47:27 +0000
+++ lib/canonical/launchpad/doc/location-widget.txt 2010-09-22 06:47:48 +0000
@@ -9,22 +9,18 @@
9 >>> salgado = getUtility(IPersonSet).getByName('salgado')9 >>> salgado = getUtility(IPersonSet).getByName('salgado')
10 >>> field = LocationField(__name__='location', title=u'Location')10 >>> field = LocationField(__name__='location', title=u'Location')
1111
12JavaScript and JSON are used in the Google Maps API. By setting the12JavaScript and JSON are used by the location widget. By setting the
13needs_gmap2 and needs_json attributes of the request, the main template13needs_json attributes of the request, the main template
14will include the necessary code to enable these features.14will include the necessary code to enable these features.
1515
16 >>> bound_field = field.bind(salgado)16 >>> bound_field = field.bind(salgado)
17 >>> request = LaunchpadTestRequest(17 >>> request = LaunchpadTestRequest(
18 ... # Let's pretend requests are coming from Brazil.18 ... # Let's pretend requests are coming from Brazil.
19 ... environ={'REMOTE_ADDR': '201.13.165.145'})19 ... environ={'REMOTE_ADDR': '201.13.165.145'})
20 >>> request.needs_gmap2
21 False
22 >>> request.needs_json20 >>> request.needs_json
23 False21 False
2422
25 >>> widget = LocationWidget(bound_field, request)23 >>> widget = LocationWidget(bound_field, request)
26 >>> request.needs_gmap2
27 True
28 >>> request.needs_json24 >>> request.needs_json
29 True25 True
3026
3127
=== modified file 'lib/canonical/launchpad/webapp/servers.py'
--- lib/canonical/launchpad/webapp/servers.py 2010-09-16 21:08:51 +0000
+++ lib/canonical/launchpad/webapp/servers.py 2010-09-22 06:47:48 +0000
@@ -522,7 +522,6 @@
522 self.needs_datepicker_iframe = False522 self.needs_datepicker_iframe = False
523 self.needs_datetimepicker_iframe = False523 self.needs_datetimepicker_iframe = False
524 self.needs_json = False524 self.needs_json = False
525 self.needs_gmap2 = False
526 super(BasicLaunchpadRequest, self).__init__(525 super(BasicLaunchpadRequest, self).__init__(
527 body_instream, environ, response)526 body_instream, environ, response)
528527
@@ -834,12 +833,10 @@
834 >>> request.needs_datepicker_iframe833 >>> request.needs_datepicker_iframe
835 False834 False
836835
837 And for JSON and GMap2:836 And for JSON:
838837
839 >>> request.needs_json838 >>> request.needs_json
840 False839 False
841 >>> request.needs_gmap2
842 False
843840
844 """841 """
845 implements(INotificationRequest, IBasicLaunchpadRequest, IParticipation,842 implements(INotificationRequest, IBasicLaunchpadRequest, IParticipation,
@@ -857,7 +854,6 @@
857 self.needs_datepicker_iframe = False854 self.needs_datepicker_iframe = False
858 self.needs_datetimepicker_iframe = False855 self.needs_datetimepicker_iframe = False
859 self.needs_json = False856 self.needs_json = False
860 self.needs_gmap2 = False
861 # stub out the FeatureController that would normally be provided by857 # stub out the FeatureController that would normally be provided by
862 # the publication mechanism858 # the publication mechanism
863 self.features = NullFeatureController()859 self.features = NullFeatureController()
864860
=== modified file 'lib/canonical/widgets/location.py'
--- lib/canonical/widgets/location.py 2010-09-11 19:25:13 +0000
+++ lib/canonical/widgets/location.py 2010-09-22 06:47:48 +0000
@@ -42,6 +42,7 @@
42 This is a single object which contains the latitude, longitude and time42 This is a single object which contains the latitude, longitude and time
43 zone of the location.43 zone of the location.
44 """44 """
45
45 def __init__(self, latitude, longitude, time_zone):46 def __init__(self, latitude, longitude, time_zone):
46 self.latitude = latitude47 self.latitude = latitude
47 self.longitude = longitude48 self.longitude = longitude
@@ -59,13 +60,16 @@
59 # json-handling, so we flag that in the request so that our60 # json-handling, so we flag that in the request so that our
60 # base-layout includes the necessary javascript files.61 # base-layout includes the necessary javascript files.
61 request.needs_json = True62 request.needs_json = True
62 request.needs_gmap2 = True
63 super(LocationWidget, self).__init__(context, request)63 super(LocationWidget, self).__init__(context, request)
64 fields = form.Fields(64 fields = form.Fields(
65 Float(__name__='latitude', title=_('Latitude'), required=False),65 Float(__name__='latitude', title=_('Latitude'), required=False),
66 Float(__name__='longitude', title=_('Longitude'), required=False),66 Float(__name__='longitude', title=_('Longitude'), required=False),
67 Choice(__name__='time_zone', vocabulary='TimezoneName',67 Choice(
68 title=_('Time zone'), required=True))68 __name__='time_zone', vocabulary='TimezoneName',
69 title=_('Time zone'), required=True,
70 description=_(
71 'Once the time zone is correctly set, events '
72 'in Launchpad will be displayed in local time.')))
69 # This will be the initial zoom level and center of the map.73 # This will be the initial zoom level and center of the map.
70 self.zoom = 274 self.zoom = 2
71 self.center_lat = 15.075 self.center_lat = 15.0
7276
=== modified file 'lib/canonical/widgets/templates/location.pt'
--- lib/canonical/widgets/templates/location.pt 2009-07-17 17:59:07 +0000
+++ lib/canonical/widgets/templates/location.pt 2010-09-22 06:47:48 +0000
@@ -2,32 +2,11 @@
2 xmlns:tal="http://xml.zope.org/namespaces/tal"2 xmlns:tal="http://xml.zope.org/namespaces/tal"
3 xmlns:i18n="http://xml.zope.org/namespaces/i18n"3 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
4 omit-tag="">4 omit-tag="">
5<tal:latitude replace="structure view/latitude_widget/hidden" />5 <tal:latitude replace="structure view/latitude_widget/hidden" />
6<tal:longitude replace="structure view/longitude_widget/hidden" />6 <tal:longitude replace="structure view/longitude_widget/hidden" />
7<p class="formHelp">7 <div>
8 You can drag the marker to change the location, and zoom into the map to8 <tal:time-zone replace="structure view/time_zone_widget" />
9 see more details and verify the location's accuracy.9 </div>
10 If your mouse has a scroll wheel, use it to more quickly zoom the10 <div class="formHelp"
11 map in and out. Double-clicking will also zoom in and move the11 tal:content="view/time_zone_widget/hint" />
12 marker. Please <strong>do not disclose sensitive information such
13 as a specific home location</strong> without the permission of
14 the person involved - rather just indicate a city so that the time
15 zone is correct.
16</p>
17<p id="map_div" style="width: 100%; height: 300px; border: 1px; float: left;"
18 ></p>
19
20<tal:render-map replace="structure view/map_javascript" />
21
22<p>
23 <label>Time zone:
24 <img id="tz_spinner" src="/@@/nospin" width="14" height="14" />
25 </label>
26 <tal:latitude replace="structure view/time_zone_widget" />
27</p>
28
29<p class="formHelp">
30 Once the time zone is correctly set, events in Launchpad will be
31 displayed in local time.
32</p>
33</tal:root>12</tal:root>
3413
=== removed file 'lib/lp/app/javascript/mapping.js'
--- lib/lp/app/javascript/mapping.js 2010-07-15 10:55:27 +0000
+++ lib/lp/app/javascript/mapping.js 1970-01-01 00:00:00 +0000
@@ -1,365 +0,0 @@
1/**
2 * Launchpad mapping tools.
3 *
4 * Map rendering and marker creation depends on the Google GMap2 library.
5 *
6 * @module lp.app.mapping
7 * @namespace lp.app.mapping
8 * @required Google GMap2
9 */
10YUI.add('lp.app.mapping', function(Y) {
11 var module = Y.namespace('lp.app.mapping');
12
13 module.RETURN_FALSE = function() {return false;};
14 module.RETURN_NULL = function() {return null;};
15
16 // Replace the crucial GMap functions so that the supporting functions
17 // will work if the GMap script is not loaded.
18 var gBrowserIsCompatible = module.RETURN_FALSE;
19 var gDownloadUrl = module.RETURN_NULL;
20
21 module.has_gmaps = (typeof(GBrowserIsCompatible) == 'function');
22
23 if (module.has_gmaps) {
24 // The GMap2 is is loaded; use the real functions.
25 // jslint does not like functions that look like classes.
26 gBrowserIsCompatible = GBrowserIsCompatible;
27 gDownloadUrl = GDownloadUrl;
28 }
29
30
31 /**
32 * Add a marker for each participant.
33 *
34 * @function setMarkersInfoWindow
35 * @param {String} data the participant XML.
36 * @param {GMap2} map the Google map to add the markers to.
37 * @param {GLatLngBounds} required_bounds the boundaries or null.
38 * @param {limit} optional max number of markers to set.
39 */
40 module.setMarkersInfoWindow = function(data, map, required_bounds,
41 limit) {
42 var xml = GXml.parse(data);
43 var markers = xml.documentElement.getElementsByTagName("participant");
44 var participant = null;
45
46 function attrToProp(attr) {
47 participant[attr.name] = attr.value;
48 }
49
50 limit = typeof(limit) == 'number' ? limit : markers.length;
51 if (markers.length < limit) {
52 limit = markers.length;
53 }
54
55 for (var i = 0; i < limit; i++) {
56 participant = {};
57 Y.Array.each(markers[i].attributes, attrToProp);
58 var point = new GLatLng(
59 parseFloat(participant.lat), parseFloat(participant.lng));
60 if (required_bounds) {
61 required_bounds.extend(point);
62 }
63 var marker = new GMarker(point);
64 marker.bindInfoWindowHtml(Y.substitute([
65 '<div style="text-align: center">',
66 '<a href="{url}">{displayname} ({name})</a><br />',
67 '{logo_html}<br />',
68 'Local time: {local_time}</div>'].join(""),
69 participant));
70 map.addOverlay(marker);
71 }
72 };
73
74 /**
75 * Add a marker for each participant, and update the zoom level.
76 *
77 * @function setMarkersInfoWindowForSmallMap
78 * @param {String} data the participant XML.
79 * @param {GMap2} map the Google map to add the markers to.
80 * @param {limit} optional max number of markers to set.
81 */
82 module.setMarkersInfoWindowForSmallMap = function(data, map, limit) {
83 var required_bounds = new GLatLngBounds();
84 module.setMarkersInfoWindow(data, map, required_bounds, limit);
85 var zoom_level = map.getBoundsZoomLevel(required_bounds);
86 // Some browsers do not display the map when the zoom_level is at the
87 // end of the range, reduce the zoom_level by 1.
88 zoom_level = Math.min(4, zoom_level - 1);
89 map.setZoom(zoom_level);
90 };
91
92 /**
93 * Set the timezone field to the lat-log location.
94 *
95 * @function setLocation
96 * @param {Number} lat a GLatLng.lat bounded number.
97 * @param {Number} lng a GLatLng.lng bounded number.
98 * @parma {String} geoname the user-name to make geonames requests.
99 * @param {String} tz_name the id of the timezone field.
100 * @param {String} lat_name the id of the latitude field.
101 * @param {String} lng_name the id of the longitude field.
102 */
103 module.setLocation = function(lat, lng, geoname,
104 tz_name, lat_name, lng_name) {
105 Y.one(Y.DOM.byId(lat_name)).set('value', lat);
106 Y.one(Y.DOM.byId(lng_name)).set('value', lng);
107 var spinner = Y.one('#tz_spinner');
108 spinner.set('src', '/@@/spinner');
109
110 function succeeded() {
111 if (request.readyState == 4) {
112 if (request.responseText) {
113 var tz = request.responseJSON.timezoneId;
114 Y.one(Y.DOM.byId(tz_name)).set('value', tz);
115 spinner.set('src', '/@@/nospin');
116 }
117 }
118 }
119
120 var url = 'http://ba-ws.geonames.net/timezoneJSON' +
121 '?username=' + geoname + '&lat=' + lat + '&lng=' + lng;
122 // This is a cross-site script request.
123 var request = new JSONScriptRequest();
124 request.open("GET", url);
125 request.onreadystatechange = succeeded;
126 request.send(null);
127 };
128
129 /**
130 * Show/hide all small maps in pages.
131 *
132 * The state is stored as ``small_maps`` in the launchpad_views cookie.
133 *
134 * @function toggleShowSmallMaps
135 * @param {Event} e the event for this callback.
136 */
137 module.toggleShowSmallMaps = function (checkbox) {
138 var is_shown = checkbox.get('checked');
139 Y.lp.launchpad_views.set('small_maps', is_shown);
140 var display = is_shown ? 'block' : 'none';
141 var maps = Y.all('.small-map');
142 maps.each(function(map) {map.setStyle('display', display);});
143 if (is_shown && !module.has_gmaps) {
144 // The server must add the Google GMap2 dependencies to the page.
145 window.location.reload();
146 }
147 };
148
149 /**
150 * Add a checkbox to show/hide all small maps in pages.
151 *
152 * @function setupShowSmallMapsControl
153 * @param {String} div_id the CSS3 id of the div that controls the map.
154 */
155 module.setupShowSmallMapsControl = function (div_id) {
156 var show_small_maps = Y.lp.launchpad_views.get('small_maps');
157 var checkbox = Y.Node.create(
158 '<input type="checkbox" name="show_small_maps" />');
159 checkbox.set(
160 'checked', show_small_maps);
161 checkbox.on(
162 'click', function(e) {module.toggleShowSmallMaps(checkbox);});
163 var label_text = Y.Node.create('Display map');
164 var label = Y.Node.create('<label></label>');
165 label.appendChild(checkbox);
166 label.appendChild(label_text);
167 var action_div = Y.one(div_id);
168 action_div.appendChild(label);
169 if (!show_small_maps) {
170 module.toggleShowSmallMaps(checkbox);
171 }
172 };
173
174 /**
175 * Create a small map with the launchpad default configuration.
176 *
177 * @function getSmallMap
178 * @param {String} div_id the id of the map div.
179 * @param {Number} center_lat a GLatLng.lat bounded number.
180 * @param {Number} center_lng a GLatLng.lng bounded number.
181 * @return {GMap2} the Google map
182 */
183 module.getSmallMap = function(div_id, center_lat, center_lng) {
184 var mapdiv = Y.DOM.byId(div_id);
185 mapdiv.style.width = '400px';
186 var map = new GMap2(mapdiv);
187 var center = new GLatLng(center_lat, center_lng);
188 map.setCenter(center, 1);
189 map.setMapType(G_NORMAL_MAP);
190 return map;
191 };
192
193 /**
194 * Create a small map of where a person is located.
195 *
196 * @function renderPersonMapSmall
197 * @param {Number} center_lat a GLatLng.lat bounded number.
198 * @param {Number} center_lng a GLatLng.lng bounded number.
199 */
200 module.renderPersonMapSmall = function(center_lat, center_lng) {
201 module.setupShowSmallMapsControl('#person_map_actions');
202 if (!gBrowserIsCompatible()) {
203 return;
204 }
205 var map = module.getSmallMap(
206 'person_map_div', center_lat, center_lng);
207 map.addControl(new GSmallZoomControl());
208 var center = new GLatLng(center_lat, center_lng);
209 var marker = new GMarker(center);
210 map.addOverlay(marker);
211 };
212
213 /**
214 * Create a small map of where a team's members are located. The map is
215 * limited to 24 members.
216 *
217 * @function renderTeamMapSmall
218 * @param {Number} center_lat a GLatLng.lat bounded number.
219 * @param {Number} center_lng a GLatLng.lng bounded number.
220 */
221 module.renderTeamMapSmall = function(center_lat, center_lng) {
222 module.setupShowSmallMapsControl('#team_map_actions');
223 if (!gBrowserIsCompatible()) {
224 return;
225 }
226 var team_map = module.getSmallMap(
227 'team_map_div', center_lat, center_lng);
228 gDownloadUrl("+mapdataltd", function(data) {
229 module.setMarkersInfoWindowForSmallMap(data, team_map);
230 });
231 };
232
233 /**
234 * Create a large map with the launchpad default configuration.
235 *
236 * @function getLargeMap
237 * @param {String} div_id the id of the map div.
238 * @return {GMap2} The Google map
239 */
240 module.getLargeMap = function(div_id) {
241 var mapdiv = Y.DOM.byId(div_id);
242 var mapheight = (parseInt(mapdiv.offsetWidth, 10) / 16 * 9);
243 mapheight = Math.min(mapheight, Y.DOM.winHeight() - 180);
244 mapheight = Math.max(mapheight, 400);
245 mapdiv.style.height = mapheight + 'px';
246 var map = new GMap2(mapdiv);
247 map.setMapType(G_HYBRID_MAP);
248 map.addControl(new GLargeMapControl());
249 map.addControl(new GMapTypeControl());
250 map.addControl(new GScaleControl());
251 map.enableScrollWheelZoom();
252 var overview_control = new GOverviewMapControl();
253 map.addControl(overview_control);
254 GEvent.addListener(map, 'zoomend', function(old, current) {
255 try {
256 if (current < 3) {
257 overview_control.hide();
258 } else {
259 overview_control.show();
260 }
261 } catch(e) {
262 // Google removed this undocumented method.
263 }
264 });
265 return map;
266 };
267
268 /**
269 * Create a large map of where a team's members are located.
270 *
271 * @function renderTeamMap
272 * @param {Number} min_lat the minimum GLatLng.lat bounded number.
273 * @param {Number} max_lat the maximum GLatLng.lat bounded number.
274 * @param {Number} min_lng the minimum GLatLng.lng bounded number.
275 * @param {Number} max_lng the maximum GLatLng.lng bounded number.
276 * @param {Number} center_lat a GLatLng.lat bounded number.
277 * @param {Number} center_lng a GLatLng.lng bounded number.
278 */
279 module.renderTeamMap = function(min_lat, max_lat, min_lng, max_lng,
280 center_lat, center_lng) {
281 if (!gBrowserIsCompatible()) {
282 return;
283 }
284 var team_map = module.getLargeMap("team_map_div");
285 var center = new GLatLng(center_lat, center_lng);
286 team_map.setCenter(center, 0);
287 var sw = new GLatLng(min_lat, min_lng);
288 var ne = new GLatLng(max_lat, max_lng);
289 var required_bounds = new GLatLngBounds(sw, ne);
290 var zoom_level = team_map.getBoundsZoomLevel(required_bounds);
291 // Some browsers do not display the map when the zoom_level is at
292 // the end of the range, reduce the zoom_level by 1.
293 zoom_level = Math.min(
294 G_HYBRID_MAP.getMaximumResolution(), zoom_level - 1);
295 team_map.setZoom(zoom_level);
296 gDownloadUrl("+mapdata", function(data) {
297 module.setMarkersInfoWindow(data, team_map);
298 });
299 };
300
301 /**
302 * Create a large, markable map of where a person is located.
303 *
304 * @function renderPersonMap
305 * @param {Number} center_lat a GLatLng.lat bounded number.
306 * @param {Number} center_lng a GLatLng.lng bounded number.
307 * @param {String} displayname the user's display name.
308 * @param {String} name the user's launchpad id.
309 * @param {String} logo_html the markup to display the user's logo.
310 * @param {String} geoname the identity used to access ba-ws.geonames.net.
311 * @param {String} tz_name the id of the timezone field.
312 * @param {String} lat_name the id of the latitude field.
313 * @param {String} lng_name the id of the longitude field.
314 * @param {number} zoom the initial zoom-level.
315 * @param {Boolean} show_marker Show the marker for the person.
316 */
317 module.renderPersonMap = function(center_lat, center_lng, displayname,
318 name, logo_html, geoname, lat_name,
319 lng_name, tz_name, zoom, show_marker) {
320 if (!gBrowserIsCompatible()) {
321 return;
322 }
323 var map = module.getLargeMap('map_div');
324 var center = new GLatLng(center_lat, center_lng);
325 map.setCenter(center, zoom);
326 var marker = new GMarker(center, {draggable: true});
327 marker.bindInfoWindowHtml(Y.substitute(
328 '<div style="text-align: center">' +
329 '<strong>{displayname}</strong><br />' +
330 '{logo_html}<br />({name})</div>',
331 {displayname: displayname, logo_html: logo_html, name: name}),
332 {maxWidth: 120});
333
334 GEvent.addListener(marker, "dragend", function() {
335 var point = marker.getLatLng();
336 module.setLocation(
337 point.lat(), point.lng(), geoname,
338 tz_name, lat_name, lng_name);
339 });
340
341 GEvent.addListener(marker, "dragstart", function() {
342 marker.closeInfoWindow();
343 });
344
345 map.addOverlay(marker);
346 if (!show_marker) {
347 marker.hide();
348 }
349
350 GEvent.addListener(map, "zoomend", function() {
351 marker.closeInfoWindow();
352 });
353
354 GEvent.addListener(map, "click", function(overlay, point) {
355 marker.setPoint(point);
356 if (marker.isHidden()) {
357 marker.show();
358 map.panTo(point);
359 }
360 module.setLocation(
361 point.lat(), point.lng(), geoname,
362 tz_name, lat_name, lng_name);
363 });
364 };
365}, "0.1", {"requires":["node", "dom", "substitute", "lp"]});
3660
=== modified file 'lib/lp/app/templates/base-layout-macros.pt'
--- lib/lp/app/templates/base-layout-macros.pt 2010-08-20 13:33:51 +0000
+++ lib/lp/app/templates/base-layout-macros.pt 2010-09-22 06:47:48 +0000
@@ -179,8 +179,6 @@
179 <script type="text/javascript"179 <script type="text/javascript"
180 tal:attributes="src string:${lp_js}/app/picker.js"></script>180 tal:attributes="src string:${lp_js}/app/picker.js"></script>
181 <script type="text/javascript"181 <script type="text/javascript"
182 tal:attributes="src string:${lp_js}/app/mapping.js"></script>
183 <script type="text/javascript"
184 tal:attributes="src string:${lp_js}/bugs/bugtracker_overlay.js"></script>182 tal:attributes="src string:${lp_js}/bugs/bugtracker_overlay.js"></script>
185 <script type="text/javascript"183 <script type="text/javascript"
186 tal:attributes="src string:${lp_js}/registry/milestoneoverlay.js"></script>184 tal:attributes="src string:${lp_js}/registry/milestoneoverlay.js"></script>
@@ -287,14 +285,6 @@
287 <script type="text/javascript"285 <script type="text/javascript"
288 tal:attributes="src string:${icingroot_contrib}/JSONScriptRequest.js"></script>286 tal:attributes="src string:${icingroot_contrib}/JSONScriptRequest.js"></script>
289 </tal:needs_json>287 </tal:needs_json>
290 <tal:needs-gmap2 condition="request/needs_gmap2">
291 <script type="text/javascript"
292 tal:condition="devmode"
293 tal:attributes="src string:http://maps.google.com/maps?${map_query};"></script>
294 <script type="text/javascript"
295 tal:condition="not: devmode"
296 tal:attributes="src string:https://maps-api-ssl.google.com/maps?oe=utf-8&amp;client=gme-canonical${map_query};"></script>
297 </tal:needs-gmap2>
298288
299 <metal:load-lavascript use-macro="context/@@+base-layout-macros/load-javascript" />289 <metal:load-lavascript use-macro="context/@@+base-layout-macros/load-javascript" />
300290
301291
=== modified file 'lib/lp/code/browser/branchmergeproposal.py'
--- lib/lp/code/browser/branchmergeproposal.py 2010-08-24 10:45:57 +0000
+++ lib/lp/code/browser/branchmergeproposal.py 2010-09-22 06:47:48 +0000
@@ -32,7 +32,6 @@
32 'latest_proposals_for_each_branch',32 'latest_proposals_for_each_branch',
33 ]33 ]
3434
35from collections import defaultdict
36import operator35import operator
3736
38from lazr.delegates import delegates37from lazr.delegates import delegates
@@ -200,7 +199,7 @@
200 'Approved [Merge Failed]',199 'Approved [Merge Failed]',
201 BranchMergeProposalStatus.QUEUED : 'Queued',200 BranchMergeProposalStatus.QUEUED : 'Queued',
202 BranchMergeProposalStatus.SUPERSEDED : 'Superseded'201 BranchMergeProposalStatus.SUPERSEDED : 'Superseded'
203 }202 }
204 return friendly_texts[self.context.queue_status]203 return friendly_texts[self.context.queue_status]
205204
206 @property205 @property
@@ -212,8 +211,7 @@
212 result = ''211 result = ''
213 if self.context.queue_status in (212 if self.context.queue_status in (
214 BranchMergeProposalStatus.CODE_APPROVED,213 BranchMergeProposalStatus.CODE_APPROVED,
215 BranchMergeProposalStatus.REJECTED214 BranchMergeProposalStatus.REJECTED):
216 ):
217 formatter = DateTimeFormatterAPI(self.context.date_reviewed)215 formatter = DateTimeFormatterAPI(self.context.date_reviewed)
218 result = '%s %s' % (216 result = '%s %s' % (
219 self.context.reviewer.displayname,217 self.context.reviewer.displayname,
@@ -601,46 +599,15 @@
601 """Location of page for commenting on this proposal."""599 """Location of page for commenting on this proposal."""
602 return canonical_url(self.context, view_name='+comment')600 return canonical_url(self.context, view_name='+comment')
603601
604 @property
605 def revision_end_date(self):
606 """The cutoff date for showing revisions.
607
608 If the proposal has been merged, then we stop at the merged date. If
609 it is rejected, we stop at the reviewed date. For superseded
610 proposals, it should ideally use the non-existant date_last_modified,
611 but could use the last comment date.
612 """
613 status = self.context.queue_status
614 if status == BranchMergeProposalStatus.MERGED:
615 return self.context.date_merged
616 if status == BranchMergeProposalStatus.REJECTED:
617 return self.context.date_reviewed
618 # Otherwise return None representing an open end date.
619 return None
620
621 def _getRevisionsSinceReviewStart(self):
622 """Get the grouped revisions since the review started."""
623 # Work out the start of the review.
624 start_date = self.context.date_review_requested
625 if start_date is None:
626 start_date = self.context.date_created
627 source = DecoratedBranch(self.context.source_branch)
628 resultset = source.getMainlineBranchRevisions(
629 start_date, self.revision_end_date, oldest_first=True)
630 # Now group by date created.
631 groups = defaultdict(list)
632 for branch_revision, revision, revision_author in resultset:
633 groups[revision.date_created].append(branch_revision)
634 return [
635 CodeReviewNewRevisions(revisions, date, source)
636 for date, revisions in groups.iteritems()]
637
638 @cachedproperty602 @cachedproperty
639 def conversation(self):603 def conversation(self):
640 """Return a conversation that is to be rendered."""604 """Return a conversation that is to be rendered."""
641 # Sort the comments by date order.605 # Sort the comments by date order.
642 comments = self._getRevisionsSinceReviewStart()
643 merge_proposal = self.context606 merge_proposal = self.context
607 groups = merge_proposal.getRevisionsSinceReviewStart()
608 source = DecoratedBranch(merge_proposal.source_branch)
609 comments = [CodeReviewNewRevisions(list(revisions), date, source)
610 for date, revisions in groups]
644 while merge_proposal is not None:611 while merge_proposal is not None:
645 from_superseded = merge_proposal != self.context612 from_superseded = merge_proposal != self.context
646 comments.extend(613 comments.extend(
@@ -946,7 +913,6 @@
946 self.cancel_url = self.next_url913 self.cancel_url = self.next_url
947 super(MergeProposalEditView, self).initialize()914 super(MergeProposalEditView, self).initialize()
948915
949
950 def _getRevisionId(self, data):916 def _getRevisionId(self, data):
951 """Translate the revision number that was entered into a revision id.917 """Translate the revision number that was entered into a revision id.
952918
@@ -1473,8 +1439,8 @@
1473 """Render an `IText` as XHTML using the webservice."""1439 """Render an `IText` as XHTML using the webservice."""
1474 formatter = FormattersAPI1440 formatter = FormattersAPI
1475 def renderer(value):1441 def renderer(value):
1476 nomail = formatter(value).obfuscate_email()1442 nomail = formatter(value).obfuscate_email()
1477 html = formatter(nomail).text_to_html()1443 html = formatter(nomail).text_to_html()
1478 return html.encode('utf-8')1444 return html.encode('utf-8')
1479 return renderer1445 return renderer
14801446
14811447
=== modified file 'lib/lp/code/browser/tests/test_branchmergeproposal.py'
--- lib/lp/code/browser/tests/test_branchmergeproposal.py 2010-08-22 21:34:16 +0000
+++ lib/lp/code/browser/tests/test_branchmergeproposal.py 2010-09-22 06:47:48 +0000
@@ -12,7 +12,6 @@
12 timedelta,12 timedelta,
13 )13 )
14from difflib import unified_diff14from difflib import unified_diff
15import operator
16import unittest15import unittest
1716
18import pytz17import pytz
@@ -46,7 +45,6 @@
46 PreviewDiff,45 PreviewDiff,
47 StaticDiff,46 StaticDiff,
48 )47 )
49from lp.code.tests.helpers import add_revision_to_branch
50from lp.testing import (48from lp.testing import (
51 login_person,49 login_person,
52 TestCaseWithFactory,50 TestCaseWithFactory,
@@ -577,63 +575,6 @@
577 view = create_initialized_view(self.bmp, '+index')575 view = create_initialized_view(self.bmp, '+index')
578 self.assertEqual([], view.linked_bugs)576 self.assertEqual([], view.linked_bugs)
579577
580 def test_revision_end_date_active(self):
581 # An active merge proposal will have None as an end date.
582 bmp = self.factory.makeBranchMergeProposal()
583 view = create_initialized_view(bmp, '+index')
584 self.assertIs(None, view.revision_end_date)
585
586 def test_revision_end_date_merged(self):
587 # An merged proposal will have the date merged as an end date.
588 bmp = self.factory.makeBranchMergeProposal(
589 set_state=BranchMergeProposalStatus.MERGED)
590 view = create_initialized_view(bmp, '+index')
591 self.assertEqual(bmp.date_merged, view.revision_end_date)
592
593 def test_revision_end_date_rejected(self):
594 # An rejected proposal will have the date reviewed as an end date.
595 bmp = self.factory.makeBranchMergeProposal(
596 set_state=BranchMergeProposalStatus.REJECTED)
597 view = create_initialized_view(bmp, '+index')
598 self.assertEqual(bmp.date_reviewed, view.revision_end_date)
599
600 def assertRevisionGroups(self, bmp, expected_groups):
601 """Get the groups for the merge proposal and check them."""
602 view = create_initialized_view(bmp, '+index')
603 groups = view._getRevisionsSinceReviewStart()
604 view_groups = [
605 obj.revisions for obj in sorted(
606 groups, key=operator.attrgetter('date'))]
607 self.assertEqual(expected_groups, view_groups)
608
609 def test_getRevisionsSinceReviewStart_no_revisions(self):
610 # If there have been no revisions pushed since the start of the
611 # review, the method returns an empty list.
612 self.assertRevisionGroups(self.bmp, [])
613
614 def test_getRevisionsSinceReviewStart_groups(self):
615 # Revisions that were scanned at the same time have the same
616 # date_created. These revisions are grouped together.
617 review_date = datetime(2009, 9, 10, tzinfo=pytz.UTC)
618 bmp = self.factory.makeBranchMergeProposal(
619 date_created=review_date)
620 login_person(bmp.registrant)
621 bmp.requestReview(review_date)
622 revision_date = review_date + timedelta(days=1)
623 revisions = []
624 for date in range(2):
625 revisions.append(
626 add_revision_to_branch(
627 self.factory, bmp.source_branch, revision_date))
628 revisions.append(
629 add_revision_to_branch(
630 self.factory, bmp.source_branch, revision_date))
631 revision_date += timedelta(days=1)
632 expected_groups = [
633 [revisions[0], revisions[1]],
634 [revisions[2], revisions[3]]]
635 self.assertRevisionGroups(bmp, expected_groups)
636
637 def test_include_superseded_comments(self):578 def test_include_superseded_comments(self):
638 for x, time in zip(range(3), time_counter()):579 for x, time in zip(range(3), time_counter()):
639 if x != 0:580 if x != 0:
@@ -757,7 +698,6 @@
757698
758 layer = LaunchpadFunctionalLayer699 layer = LaunchpadFunctionalLayer
759700
760
761 def _makeCommentFromEmailWithAttachment(self, attachment_body):701 def _makeCommentFromEmailWithAttachment(self, attachment_body):
762 # Make an email message with an attachment, and create a code702 # Make an email message with an attachment, and create a code
763 # review comment from it.703 # review comment from it.
764704
=== modified file 'lib/lp/code/configure.zcml'
--- lib/lp/code/configure.zcml 2010-09-20 22:16:32 +0000
+++ lib/lp/code/configure.zcml 2010-09-22 06:47:48 +0000
@@ -236,8 +236,10 @@
236 votes236 votes
237 all_comments237 all_comments
238 related_bugs238 related_bugs
239 revision_end_date
239 isMergable240 isMergable
240 getComment241 getComment
242 getRevisionsSinceReviewStart
241 getNotificationRecipients243 getNotificationRecipients
242 getVoteReference244 getVoteReference
243 isValidTransition245 isValidTransition
244246
=== modified file 'lib/lp/code/interfaces/branchmergeproposal.py'
--- lib/lp/code/interfaces/branchmergeproposal.py 2010-08-20 20:31:18 +0000
+++ lib/lp/code/interfaces/branchmergeproposal.py 2010-09-22 06:47:48 +0000
@@ -290,6 +290,13 @@
290 def getComment(id):290 def getComment(id):
291 """Return the CodeReviewComment with the specified ID."""291 """Return the CodeReviewComment with the specified ID."""
292292
293 def getRevisionsSinceReviewStart():
294 """Return all the revisions added since the review began.
295
296 Revisions are grouped by creation (i.e. push) time.
297 :return: An iterator of (date, iterator of revision data)
298 """
299
293 def getVoteReference(id):300 def getVoteReference(id):
294 """Return the CodeReviewVoteReference with the specified ID."""301 """Return the CodeReviewVoteReference with the specified ID."""
295302
@@ -518,8 +525,8 @@
518 source branch.525 source branch.
519 :param target_revision_id: The revision id that was used from the526 :param target_revision_id: The revision id that was used from the
520 target branch.527 target branch.
521 :param prerequisite_revision_id: The revision id that was used from the528 :param prerequisite_revision_id: The revision id that was used from
522 prerequisite branch.529 the prerequisite branch.
523 :param conflicts: Text describing the conflicts if any.530 :param conflicts: Text describing the conflicts if any.
524 """531 """
525532
526533
=== modified file 'lib/lp/code/interfaces/revision.py'
--- lib/lp/code/interfaces/revision.py 2010-08-20 20:31:18 +0000
+++ lib/lp/code/interfaces/revision.py 2010-09-22 06:47:48 +0000
@@ -73,6 +73,9 @@
73 :return: A `Branch` or None if an appropriate branch cannot be found.73 :return: A `Branch` or None if an appropriate branch cannot be found.
74 """74 """
7575
76 def getLefthandParent():
77 """Return lefthand parent of revision, or None if not in database."""
78
7679
77class IRevisionAuthor(Interface):80class IRevisionAuthor(Interface):
78 """Committer of a Bazaar revision."""81 """Committer of a Bazaar revision."""
7982
=== modified file 'lib/lp/code/model/branch.py'
--- lib/lp/code/model/branch.py 2010-09-19 22:32:31 +0000
+++ lib/lp/code/model/branch.py 2010-09-22 06:47:48 +0000
@@ -990,8 +990,8 @@
990 increment = getUtility(IBranchPuller).MIRROR_TIME_INCREMENT990 increment = getUtility(IBranchPuller).MIRROR_TIME_INCREMENT
991 self.next_mirror_time = (991 self.next_mirror_time = (
992 datetime.now(pytz.timezone('UTC')) + increment)992 datetime.now(pytz.timezone('UTC')) + increment)
993 if self.last_mirrored_id != last_revision_id:993 self.last_mirrored_id = last_revision_id
994 self.last_mirrored_id = last_revision_id994 if self.last_scanned_id != last_revision_id:
995 from lp.code.model.branchjob import BranchScanJob995 from lp.code.model.branchjob import BranchScanJob
996 BranchScanJob.create(self)996 BranchScanJob.create(self)
997 self.control_format = control_format997 self.control_format = control_format
998998
=== modified file 'lib/lp/code/model/branchmergeproposal.py'
--- lib/lp/code/model/branchmergeproposal.py 2010-09-13 04:36:24 +0000
+++ lib/lp/code/model/branchmergeproposal.py 2010-09-22 06:47:48 +0000
@@ -13,7 +13,7 @@
13 ]13 ]
1414
15from email.Utils import make_msgid15from email.Utils import make_msgid
1616from itertools import groupby
17from sqlobject import (17from sqlobject import (
18 ForeignKey,18 ForeignKey,
19 IntCol,19 IntCol,
@@ -777,6 +777,45 @@
777 Store.of(self).flush()777 Store.of(self).flush()
778 return self.preview_diff778 return self.preview_diff
779779
780 @property
781 def revision_end_date(self):
782 """The cutoff date for showing revisions.
783
784 If the proposal has been merged, then we stop at the merged date. If
785 it is rejected, we stop at the reviewed date. For superseded
786 proposals, it should ideally use the non-existant date_last_modified,
787 but could use the last comment date.
788 """
789 status = self.queue_status
790 if status == BranchMergeProposalStatus.MERGED:
791 return self.date_merged
792 if status == BranchMergeProposalStatus.REJECTED:
793 return self.date_reviewed
794 # Otherwise return None representing an open end date.
795 return None
796
797 def _getNewerRevisions(self):
798 start_date = self.date_review_requested
799 if start_date is None:
800 start_date = self.date_created
801 return self.source_branch.getMainlineBranchRevisions(
802 start_date, self.revision_end_date, oldest_first=True)
803
804 def getRevisionsSinceReviewStart(self):
805 """Get the grouped revisions since the review started."""
806 resultset = self._getNewerRevisions()
807 # Work out the start of the review.
808 branch_revisions = (
809 branch_revision for branch_revision, revision, revision_author
810 in resultset)
811 # Now group by date created.
812 gby = groupby(branch_revisions, lambda r: r.revision.date_created)
813 # Use a generator expression to wrap the custom iterator so it doesn't
814 # get security-proxied.
815 return (
816 (date, (revision for revision in revisions))
817 for date, revisions in gby)
818
780819
781class BranchMergeProposalGetter:820class BranchMergeProposalGetter:
782 """See `IBranchMergeProposalGetter`."""821 """See `IBranchMergeProposalGetter`."""
783822
=== modified file 'lib/lp/code/model/directbranchcommit.py'
--- lib/lp/code/model/directbranchcommit.py 2010-08-20 20:31:18 +0000
+++ lib/lp/code/model/directbranchcommit.py 2010-09-22 06:47:48 +0000
@@ -55,7 +55,8 @@
55 is_locked = False55 is_locked = False
56 commit_builder = None56 commit_builder = None
5757
58 def __init__(self, db_branch, committer=None, no_race_check=False):58 def __init__(self, db_branch, committer=None, no_race_check=False,
59 merge_parents=None):
59 """Create context for direct commit to branch.60 """Create context for direct commit to branch.
6061
61 Before constructing a `DirectBranchCommit`, set up a server that62 Before constructing a `DirectBranchCommit`, set up a server that
@@ -107,6 +108,7 @@
107 raise108 raise
108109
109 self.files = set()110 self.files = set()
111 self.merge_parents = merge_parents
110112
111 def _getDir(self, path):113 def _getDir(self, path):
112 """Get trans_id for directory "path." Create if necessary."""114 """Get trans_id for directory "path." Create if necessary."""
@@ -200,7 +202,8 @@
200 # required to generate the revision-id.202 # required to generate the revision-id.
201 with override_environ(BZR_EMAIL=committer_id):203 with override_environ(BZR_EMAIL=committer_id):
202 new_rev_id = self.transform_preview.commit(204 new_rev_id = self.transform_preview.commit(
203 self.bzrbranch, commit_message, committer=committer_id)205 self.bzrbranch, commit_message, self.merge_parents,
206 committer=committer_id)
204 IMasterObject(self.db_branch).branchChanged(207 IMasterObject(self.db_branch).branchChanged(
205 get_stacked_on_url(self.bzrbranch), new_rev_id,208 get_stacked_on_url(self.bzrbranch), new_rev_id,
206 self.db_branch.control_format, self.db_branch.branch_format,209 self.db_branch.control_format, self.db_branch.branch_format,
207210
=== modified file 'lib/lp/code/model/revision.py'
--- lib/lp/code/model/revision.py 2010-08-27 02:11:36 +0000
+++ lib/lp/code/model/revision.py 2010-09-22 06:47:48 +0000
@@ -18,6 +18,7 @@
18 )18 )
19import email19import email
2020
21from bzrlib.revision import NULL_REVISION
21import pytz22import pytz
22from sqlobject import (23from sqlobject import (
23 BoolCol,24 BoolCol,
@@ -39,7 +40,6 @@
39 )40 )
40from storm.locals import (41from storm.locals import (
41 Bool,42 Bool,
42 DateTime,
43 Int,43 Int,
44 Min,44 Min,
45 Reference,45 Reference,
@@ -118,6 +118,13 @@
118 """118 """
119 return [parent.parent_id for parent in self.parents]119 return [parent.parent_id for parent in self.parents]
120120
121 def getLefthandParent(self):
122 if len(self.parent_ids) == 0:
123 parent_id = NULL_REVISION
124 else:
125 parent_id = self.parent_ids[0]
126 return RevisionSet().getByRevisionId(parent_id)
127
121 def getProperties(self):128 def getProperties(self):
122 """See `IRevision`."""129 """See `IRevision`."""
123 return dict((prop.name, prop.value) for prop in self.properties)130 return dict((prop.name, prop.value) for prop in self.properties)
124131
=== modified file 'lib/lp/code/model/tests/test_branch.py'
--- lib/lp/code/model/tests/test_branch.py 2010-09-09 02:49:47 +0000
+++ lib/lp/code/model/tests/test_branch.py 2010-09-22 06:47:48 +0000
@@ -231,11 +231,25 @@
231 branch = self.factory.makeAnyBranch()231 branch = self.factory.makeAnyBranch()
232 login_person(branch.owner)232 login_person(branch.owner)
233 removeSecurityProxy(branch).last_mirrored_id = 'rev1'233 removeSecurityProxy(branch).last_mirrored_id = 'rev1'
234 jobs = list(getUtility(IBranchScanJobSource).iterReady())234 removeSecurityProxy(branch).last_scanned_id = 'rev1'
235 self.assertEqual(0, len(jobs))235 jobs = list(getUtility(IBranchScanJobSource).iterReady())
236 branch.branchChanged('', 'rev1', *self.arbitrary_formats)236 self.assertEqual(0, len(jobs))
237 jobs = list(getUtility(IBranchScanJobSource).iterReady())237 branch.branchChanged('', 'rev1', *self.arbitrary_formats)
238 self.assertEqual(0, len(jobs))238 jobs = list(getUtility(IBranchScanJobSource).iterReady())
239 self.assertEqual(0, len(jobs))
240
241 def test_branchChanged_creates_scan_job_for_broken_scan(self):
242 # branchChanged() if the last_scanned_id is different to the newly
243 # changed revision, then a scan job is created.
244 branch = self.factory.makeAnyBranch()
245 login_person(branch.owner)
246 removeSecurityProxy(branch).last_mirrored_id = 'rev1'
247 removeSecurityProxy(branch).last_scanned_id = 'old'
248 jobs = list(getUtility(IBranchScanJobSource).iterReady())
249 self.assertEqual(0, len(jobs))
250 branch.branchChanged('', 'rev1', *self.arbitrary_formats)
251 jobs = list(getUtility(IBranchScanJobSource).iterReady())
252 self.assertEqual(1, len(jobs))
239253
240 def test_branchChanged_packs_format(self):254 def test_branchChanged_packs_format(self):
241 # branchChanged sets the branch_format etc attributes to the passed in255 # branchChanged sets the branch_format etc attributes to the passed in
242256
=== modified file 'lib/lp/code/model/tests/test_branchmergeproposal.py'
--- lib/lp/code/model/tests/test_branchmergeproposal.py 2010-08-20 20:31:18 +0000
+++ lib/lp/code/model/tests/test_branchmergeproposal.py 2010-09-22 06:47:48 +0000
@@ -7,7 +7,7 @@
77
8__metaclass__ = type8__metaclass__ = type
99
10from datetime import datetime10from datetime import datetime, timedelta
11from difflib import unified_diff11from difflib import unified_diff
12from unittest import (12from unittest import (
13 TestCase,13 TestCase,
@@ -72,6 +72,7 @@
72 MergeProposalCreatedJob,72 MergeProposalCreatedJob,
73 UpdatePreviewDiffJob,73 UpdatePreviewDiffJob,
74 )74 )
75from lp.code.tests.helpers import add_revision_to_branch
75from lp.registry.interfaces.person import IPersonSet76from lp.registry.interfaces.person import IPersonSet
76from lp.registry.interfaces.product import IProductSet77from lp.registry.interfaces.product import IProductSet
77from lp.testing import (78from lp.testing import (
@@ -108,7 +109,8 @@
108 self.assertTrue(url.startswith(source_branch_url))109 self.assertTrue(url.startswith(source_branch_url))
109110
110 def test_BranchMergeProposal_canonical_url_rest(self):111 def test_BranchMergeProposal_canonical_url_rest(self):
111 # The rest of the URL for a merge proposal is +merge followed by the db id.112 # The rest of the URL for a merge proposal is +merge followed by the
113 # db id.
112 bmp = self.factory.makeBranchMergeProposal()114 bmp = self.factory.makeBranchMergeProposal()
113 url = canonical_url(bmp)115 url = canonical_url(bmp)
114 source_branch_url = canonical_url(bmp.source_branch)116 source_branch_url = canonical_url(bmp.source_branch)
@@ -238,7 +240,6 @@
238 self._attemptTransition,240 self._attemptTransition,
239 proposal, to_state)241 proposal, to_state)
240242
241
242 def assertGoodDupeTransition(self, from_state, to_state):243 def assertGoodDupeTransition(self, from_state, to_state):
243 """Trying to go from `from_state` to `to_state` succeeds."""244 """Trying to go from `from_state` to `to_state` succeeds."""
244 proposal = self.prepareDupeTransition(from_state)245 proposal = self.prepareDupeTransition(from_state)
@@ -1049,6 +1050,7 @@
1049 else:1050 else:
1050 self.assertEqual([mp], list(active))1051 self.assertEqual([mp], list(active))
10511052
1053
1052class TestBranchMergeProposalGetterGetProposals(TestCaseWithFactory):1054class TestBranchMergeProposalGetterGetProposals(TestCaseWithFactory):
1053 """Test the getProposalsForContext method."""1055 """Test the getProposalsForContext method."""
10541056
@@ -1118,7 +1120,6 @@
1118 beaver, [BranchMergeProposalStatus.REJECTED], beaver)1120 beaver, [BranchMergeProposalStatus.REJECTED], beaver)
1119 self.assertEqual(beave_proposals.count(), 1)1121 self.assertEqual(beave_proposals.count(), 1)
11201122
1121
1122 def test_created_proposal_default_status(self):1123 def test_created_proposal_default_status(self):
1123 # When we create a merge proposal using the helper method, the default1124 # When we create a merge proposal using the helper method, the default
1124 # status of the proposal is work in progress.1125 # status of the proposal is work in progress.
@@ -1799,5 +1800,67 @@
1799 self.assertIs(None, bmp.next_preview_diff_job)1800 self.assertIs(None, bmp.next_preview_diff_job)
18001801
18011802
1803class TestRevisionEndDate(TestCaseWithFactory):
1804
1805 layer = DatabaseFunctionalLayer
1806
1807 def test_revision_end_date_active(self):
1808 # An active merge proposal will have None as an end date.
1809 bmp = self.factory.makeBranchMergeProposal()
1810 self.assertIs(None, bmp.revision_end_date)
1811
1812 def test_revision_end_date_merged(self):
1813 # An merged proposal will have the date merged as an end date.
1814 bmp = self.factory.makeBranchMergeProposal(
1815 set_state=BranchMergeProposalStatus.MERGED)
1816 self.assertEqual(bmp.date_merged, bmp.revision_end_date)
1817
1818 def test_revision_end_date_rejected(self):
1819 # An rejected proposal will have the date reviewed as an end date.
1820 bmp = self.factory.makeBranchMergeProposal(
1821 set_state=BranchMergeProposalStatus.REJECTED)
1822 self.assertEqual(bmp.date_reviewed, bmp.revision_end_date)
1823
1824
1825class TestGetRevisionsSinceReviewStart(TestCaseWithFactory):
1826
1827 layer = DatabaseFunctionalLayer
1828
1829 def assertRevisionGroups(self, bmp, expected_groups):
1830 """Get the groups for the merge proposal and check them."""
1831 groups = bmp.getRevisionsSinceReviewStart()
1832 revision_groups = [list(revisions) for date, revisions in groups]
1833 self.assertEqual(expected_groups, revision_groups)
1834
1835 def test_getRevisionsSinceReviewStart_no_revisions(self):
1836 # If there have been no revisions pushed since the start of the
1837 # review, the method returns an empty list.
1838 bmp = self.factory.makeBranchMergeProposal()
1839 self.assertRevisionGroups(bmp, [])
1840
1841 def test_getRevisionsSinceReviewStart_groups(self):
1842 # Revisions that were scanned at the same time have the same
1843 # date_created. These revisions are grouped together.
1844 review_date = datetime(2009, 9, 10, tzinfo=UTC)
1845 bmp = self.factory.makeBranchMergeProposal(
1846 date_created=review_date)
1847 login_person(bmp.registrant)
1848 bmp.requestReview(review_date)
1849 revision_date = review_date + timedelta(days=1)
1850 revisions = []
1851 for date in range(2):
1852 revisions.append(
1853 add_revision_to_branch(
1854 self.factory, bmp.source_branch, revision_date))
1855 revisions.append(
1856 add_revision_to_branch(
1857 self.factory, bmp.source_branch, revision_date))
1858 revision_date += timedelta(days=1)
1859 expected_groups = [
1860 [revisions[0], revisions[1]],
1861 [revisions[2], revisions[3]]]
1862 self.assertRevisionGroups(bmp, expected_groups)
1863
1864
1802def test_suite():1865def test_suite():
1803 return TestLoader().loadTestsFromName(__name__)1866 return TestLoader().loadTestsFromName(__name__)
18041867
=== modified file 'lib/lp/code/model/tests/test_diff.py'
--- lib/lp/code/model/tests/test_diff.py 2010-08-20 20:31:18 +0000
+++ lib/lp/code/model/tests/test_diff.py 2010-09-22 06:47:48 +0000
@@ -57,13 +57,14 @@
57class DiffTestCase(TestCaseWithFactory):57class DiffTestCase(TestCaseWithFactory):
5858
59 @staticmethod59 @staticmethod
60 def commitFile(branch, path, contents):60 def commitFile(branch, path, contents, merge_parents=None):
61 """Create a commit that updates a file to specified contents.61 """Create a commit that updates a file to specified contents.
6262
63 This will create or modify the file, as needed.63 This will create or modify the file, as needed.
64 """64 """
65 committer = DirectBranchCommit(65 committer = DirectBranchCommit(
66 removeSecurityProxy(branch), no_race_check=True)66 removeSecurityProxy(branch), no_race_check=True,
67 merge_parents=merge_parents)
67 committer.writeFile(path, contents)68 committer.writeFile(path, contents)
68 try:69 try:
69 return committer.commit('committing')70 return committer.commit('committing')
@@ -122,7 +123,6 @@
122 prerequisite)123 prerequisite)
123124
124125
125
126class TestDiff(DiffTestCase):126class TestDiff(DiffTestCase):
127127
128 layer = LaunchpadFunctionalLayer128 layer = LaunchpadFunctionalLayer
@@ -186,19 +186,19 @@
186 self.checkExampleMerge(diff.text)186 self.checkExampleMerge(diff.text)
187187
188 diff_bytes = (188 diff_bytes = (
189 "--- bar 2009-08-26 15:53:34.000000000 -0400\n"189 "--- bar\t2009-08-26 15:53:34.000000000 -0400\n"
190 "+++ bar 1969-12-31 19:00:00.000000000 -0500\n"190 "+++ bar\t1969-12-31 19:00:00.000000000 -0500\n"
191 "@@ -1,3 +0,0 @@\n"191 "@@ -1,3 +0,0 @@\n"
192 "-a\n"192 "-a\n"
193 "-b\n"193 "-b\n"
194 "-c\n"194 "-c\n"
195 "--- baz 1969-12-31 19:00:00.000000000 -0500\n"195 "--- baz\t1969-12-31 19:00:00.000000000 -0500\n"
196 "+++ baz 2009-08-26 15:53:57.000000000 -0400\n"196 "+++ baz\t2009-08-26 15:53:57.000000000 -0400\n"
197 "@@ -0,0 +1,2 @@\n"197 "@@ -0,0 +1,2 @@\n"
198 "+a\n"198 "+a\n"
199 "+b\n"199 "+b\n"
200 "--- foo 2009-08-26 15:53:23.000000000 -0400\n"200 "--- foo\t2009-08-26 15:53:23.000000000 -0400\n"
201 "+++ foo 2009-08-26 15:56:43.000000000 -0400\n"201 "+++ foo\t2009-08-26 15:56:43.000000000 -0400\n"
202 "@@ -1,3 +1,4 @@\n"202 "@@ -1,3 +1,4 @@\n"
203 " a\n"203 " a\n"
204 "-b\n"204 "-b\n"
@@ -207,19 +207,19 @@
207 "+e\n")207 "+e\n")
208208
209 diff_bytes_2 = (209 diff_bytes_2 = (
210 "--- bar 2009-08-26 15:53:34.000000000 -0400\n"210 "--- bar\t2009-08-26 15:53:34.000000000 -0400\n"
211 "+++ bar 1969-12-31 19:00:00.000000000 -0500\n"211 "+++ bar\t1969-12-31 19:00:00.000000000 -0500\n"
212 "@@ -1,3 +0,0 @@\n"212 "@@ -1,3 +0,0 @@\n"
213 "-a\n"213 "-a\n"
214 "-b\n"214 "-b\n"
215 "-c\n"215 "-c\n"
216 "--- baz 1969-12-31 19:00:00.000000000 -0500\n"216 "--- baz\t1969-12-31 19:00:00.000000000 -0500\n"
217 "+++ baz 2009-08-26 15:53:57.000000000 -0400\n"217 "+++ baz\t2009-08-26 15:53:57.000000000 -0400\n"
218 "@@ -0,0 +1,2 @@\n"218 "@@ -0,0 +1,2 @@\n"
219 "+a\n"219 "+a\n"
220 "+b\n"220 "+b\n"
221 "--- foo 2009-08-26 15:53:23.000000000 -0400\n"221 "--- foo\t2009-08-26 15:53:23.000000000 -0400\n"
222 "+++ foo 2009-08-26 15:56:43.000000000 -0400\n"222 "+++ foo\t2009-08-26 15:56:43.000000000 -0400\n"
223 "@@ -1,3 +1,5 @@\n"223 "@@ -1,3 +1,5 @@\n"
224 " a\n"224 " a\n"
225 "-b\n"225 "-b\n"
@@ -467,7 +467,6 @@
467 self.assertEqual('', diff.conflicts)467 self.assertEqual('', diff.conflicts)
468 self.assertFalse(diff.has_conflicts)468 self.assertFalse(diff.has_conflicts)
469469
470
471 def test_fromBranchMergeProposal(self):470 def test_fromBranchMergeProposal(self):
472 # Correctly generates a PreviewDiff from a BranchMergeProposal.471 # Correctly generates a PreviewDiff from a BranchMergeProposal.
473 bmp, source_rev_id, target_rev_id = self.createExampleMerge()472 bmp, source_rev_id, target_rev_id = self.createExampleMerge()
474473
=== modified file 'lib/lp/code/tests/helpers.py'
--- lib/lp/code/tests/helpers.py 2010-08-31 00:16:41 +0000
+++ lib/lp/code/tests/helpers.py 2010-09-22 06:47:48 +0000
@@ -64,9 +64,14 @@
64 """64 """
65 if date_created is None:65 if date_created is None:
66 date_created = revision_date66 date_created = revision_date
67 parent = branch.revision_history.last()
68 if parent is None:
69 parent_ids = []
70 else:
71 parent_ids = [parent.revision.revision_id]
67 revision = factory.makeRevision(72 revision = factory.makeRevision(
68 revision_date=revision_date, date_created=date_created,73 revision_date=revision_date, date_created=date_created,
69 log_body=commit_msg)74 log_body=commit_msg, parent_ids=parent_ids)
70 if mainline:75 if mainline:
71 sequence = branch.revision_count + 176 sequence = branch.revision_count + 1
72 branch_revision = branch.createBranchRevision(sequence, revision)77 branch_revision = branch.createBranchRevision(sequence, revision)
@@ -112,7 +117,7 @@
112 preview.remvoed_lines_count = 13117 preview.remvoed_lines_count = 13
113 preview.diffstat = {'file1': (3, 8), 'file2': (4, 5)}118 preview.diffstat = {'file1': (3, 8), 'file2': (4, 5)}
114 return {119 return {
115 'eric': eric, 'fooix': fooix, 'trunk':trunk, 'feature': feature,120 'eric': eric, 'fooix': fooix, 'trunk': trunk, 'feature': feature,
116 'proposed': proposed, 'fred': fred}121 'proposed': proposed, 'fred': fred}
117122
118123
119124
=== modified file 'lib/lp/code/tests/test_directbranchcommit.py'
--- lib/lp/code/tests/test_directbranchcommit.py 2010-08-20 20:31:18 +0000
+++ lib/lp/code/tests/test_directbranchcommit.py 2010-09-22 06:47:48 +0000
@@ -99,6 +99,24 @@
99 branch_revision_id = self.committer.bzrbranch.last_revision()99 branch_revision_id = self.committer.bzrbranch.last_revision()
100 self.assertEqual(branch_revision_id, revision_id)100 self.assertEqual(branch_revision_id, revision_id)
101101
102 def test_commit_uses_merge_parents(self):
103 # DirectBranchCommit.commit returns uses merge parents
104 self._tearDownCommitter()
105 # Merge parents cannot be specified for initial commit, so do an
106 # empty commit.
107 self.tree.commit('foo', committer='foo@bar', rev_id='foo')
108 committer = DirectBranchCommit(
109 self.db_branch, merge_parents=['parent-1', 'parent-2'])
110 committer.last_scanned_id = (
111 committer.bzrbranch.last_revision())
112 committer.writeFile('file.txt', 'contents')
113 revision_id = committer.commit('')
114 branch_revision_id = committer.bzrbranch.last_revision()
115 branch_revision = committer.bzrbranch.repository.get_revision(
116 branch_revision_id)
117 self.assertEqual(
118 ['parent-1', 'parent-2'], branch_revision.parent_ids[1:])
119
102 def test_DirectBranchCommit_aborts_cleanly(self):120 def test_DirectBranchCommit_aborts_cleanly(self):
103 # If a DirectBranchCommit is not committed, its changes do not121 # If a DirectBranchCommit is not committed, its changes do not
104 # go into the branch.122 # go into the branch.
105123
=== modified file 'lib/lp/codehosting/branchdistro.py'
--- lib/lp/codehosting/branchdistro.py 2010-08-20 20:31:18 +0000
+++ lib/lp/codehosting/branchdistro.py 2010-09-22 06:47:48 +0000
@@ -28,7 +28,7 @@
2828
29from canonical.config import config29from canonical.config import config
30from canonical.launchpad.interfaces import ILaunchpadCelebrities30from canonical.launchpad.interfaces import ILaunchpadCelebrities
31from lp.code.enums import BranchType31from lp.code.enums import BranchLifecycleStatus, BranchType
32from lp.code.errors import BranchExists32from lp.code.errors import BranchExists
33from lp.code.interfaces.branchcollection import IAllBranches33from lp.code.interfaces.branchcollection import IAllBranches
34from lp.code.interfaces.branchnamespace import IBranchNamespaceSet34from lp.code.interfaces.branchnamespace import IBranchNamespaceSet
@@ -341,6 +341,7 @@
341 new_db_branch.sourcepackage.setBranch(341 new_db_branch.sourcepackage.setBranch(
342 PackagePublishingPocket.RELEASE, new_db_branch,342 PackagePublishingPocket.RELEASE, new_db_branch,
343 getUtility(ILaunchpadCelebrities).ubuntu_branches.teamowner)343 getUtility(ILaunchpadCelebrities).ubuntu_branches.teamowner)
344 old_db_branch.lifecycle_status = BranchLifecycleStatus.MATURE
344 # switch_branches *moves* the data to locations dependent on the345 # switch_branches *moves* the data to locations dependent on the
345 # new_branch's id, so if the transaction was rolled back we wouldn't346 # new_branch's id, so if the transaction was rolled back we wouldn't
346 # know the branch id and thus wouldn't be able to find the branch data347 # know the branch id and thus wouldn't be able to find the branch data
347348
=== modified file 'lib/lp/codehosting/bzrutils.py'
--- lib/lp/codehosting/bzrutils.py 2010-08-20 20:31:18 +0000
+++ lib/lp/codehosting/bzrutils.py 2010-09-22 06:47:48 +0000
@@ -18,11 +18,13 @@
18 'identical_formats',18 'identical_formats',
19 'install_oops_handler',19 'install_oops_handler',
20 'is_branch_stackable',20 'is_branch_stackable',
21 'read_locked',
21 'remove_exception_logging_hook',22 'remove_exception_logging_hook',
22 'safe_open',23 'safe_open',
23 'UnsafeUrlSeen',24 'UnsafeUrlSeen',
24 ]25 ]
2526
27from contextlib import contextmanager
26import os28import os
27import sys29import sys
28import threading30import threading
@@ -363,3 +365,12 @@
363 return branch.get_stacked_on_url()365 return branch.get_stacked_on_url()
364 except (NotStacked, UnstackableBranchFormat):366 except (NotStacked, UnstackableBranchFormat):
365 return None367 return None
368
369
370@contextmanager
371def read_locked(branch):
372 branch.lock_read()
373 try:
374 yield
375 finally:
376 branch.unlock()
366377
=== modified file 'lib/lp/codehosting/tests/test_branchdistro.py'
--- lib/lp/codehosting/tests/test_branchdistro.py 2010-08-20 20:31:18 +0000
+++ lib/lp/codehosting/tests/test_branchdistro.py 2010-09-22 06:47:48 +0000
@@ -34,6 +34,7 @@
34 QuietFakeLogger,34 QuietFakeLogger,
35 )35 )
36from canonical.testing.layers import LaunchpadZopelessLayer36from canonical.testing.layers import LaunchpadZopelessLayer
37from lp.code.enums import BranchLifecycleStatus
37from lp.codehosting.branchdistro import (38from lp.codehosting.branchdistro import (
38 DistroBrancher,39 DistroBrancher,
39 switch_branches,40 switch_branches,
@@ -233,6 +234,12 @@
233 db_branch.sourcepackagename, brancher.new_distroseries.name],234 db_branch.sourcepackagename, brancher.new_distroseries.name],
234 [new_branch.owner, new_branch.distribution,235 [new_branch.owner, new_branch.distribution,
235 new_branch.sourcepackagename, new_branch.name])236 new_branch.sourcepackagename, new_branch.name])
237 # The new branch is set in the development state, and the old one is
238 # mature.
239 self.assertEqual(
240 BranchLifecycleStatus.DEVELOPMENT, new_branch.lifecycle_status)
241 self.assertEqual(
242 BranchLifecycleStatus.MATURE, db_branch.lifecycle_status)
236243
237 def test_makeOneNewBranch_inconsistent_branch(self):244 def test_makeOneNewBranch_inconsistent_branch(self):
238 # makeOneNewBranch skips over an inconsistent official package branch245 # makeOneNewBranch skips over an inconsistent official package branch
239246
=== modified file 'lib/lp/registry/browser/__init__.py'
--- lib/lp/registry/browser/__init__.py 2010-09-03 15:02:39 +0000
+++ lib/lp/registry/browser/__init__.py 2010-09-22 06:47:48 +0000
@@ -7,7 +7,6 @@
77
8__all__ = [8__all__ = [
9 'get_status_counts',9 'get_status_counts',
10 'MapMixin',
11 'MilestoneOverlayMixin',10 'MilestoneOverlayMixin',
12 'RegistryEditFormView',11 'RegistryEditFormView',
13 'RegistryDeleteViewMixin',12 'RegistryDeleteViewMixin',
@@ -32,7 +31,6 @@
32 )31 )
33from lp.registry.interfaces.productseries import IProductSeries32from lp.registry.interfaces.productseries import IProductSeries
34from lp.registry.interfaces.series import SeriesStatus33from lp.registry.interfaces.series import SeriesStatus
35from lp.services.propertycache import cachedproperty
3634
3735
38class StatusCount:36class StatusCount:
@@ -258,19 +256,3 @@
258 @action("Change", name='change')256 @action("Change", name='change')
259 def change_action(self, action, data):257 def change_action(self, action, data):
260 self.updateContextFromData(data)258 self.updateContextFromData(data)
261
262
263class MapMixin:
264
265 @cachedproperty
266 def gmap2_enabled(self):
267 # XXX sinzui 2010-08-27 bug=625556: This is a hack to use
268 # feature flags, which are not ready for general use in the production
269 # code, but has just enough to support this use case:
270 # Do not enable gmap2 if Google's service is not operational.
271 from lp.services.features.flags import FeatureController
272
273 def in_scope(value):
274 return True
275
276 return FeatureController(in_scope).getFlag('gmap2') == 'on'
277259
=== modified file 'lib/lp/registry/browser/configure.zcml'
--- lib/lp/registry/browser/configure.zcml 2010-09-13 10:04:20 +0000
+++ lib/lp/registry/browser/configure.zcml 2010-09-22 06:47:48 +0000
@@ -829,7 +829,7 @@
829 for="lp.registry.interfaces.person.IPerson"829 for="lp.registry.interfaces.person.IPerson"
830 class="lp.registry.browser.person.PersonEditLocationView"830 class="lp.registry.browser.person.PersonEditLocationView"
831 permission="launchpad.Edit"831 permission="launchpad.Edit"
832 template="../templates/person-editlocation.pt"/>832 template="../../app/templates/generic-edit.pt"/>
833 <browser:page833 <browser:page
834 name="+contactuser"834 name="+contactuser"
835 for="lp.registry.interfaces.person.IPerson"835 for="lp.registry.interfaces.person.IPerson"
@@ -1080,11 +1080,6 @@
1080 <browser:page1080 <browser:page
1081 for="lp.registry.interfaces.person.ITeam"1081 for="lp.registry.interfaces.person.ITeam"
1082 permission="zope.Public"1082 permission="zope.Public"
1083 class="lp.registry.browser.person.TeamEditLocationView"
1084 name="+editlocation"/>
1085 <browser:page
1086 for="lp.registry.interfaces.person.ITeam"
1087 permission="zope.Public"
1088 name="+listing-simple"1083 name="+listing-simple"
1089 template="../templates/team-listing-simple.pt"/>1084 template="../templates/team-listing-simple.pt"/>
1090 <browser:page1085 <browser:page
10911086
=== modified file 'lib/lp/registry/browser/person.py'
--- lib/lp/registry/browser/person.py 2010-09-19 00:35:22 +0000
+++ lib/lp/registry/browser/person.py 2010-09-22 06:47:48 +0000
@@ -65,7 +65,6 @@
65 'SearchSubscribedQuestionsView',65 'SearchSubscribedQuestionsView',
66 'TeamAddMyTeamsView',66 'TeamAddMyTeamsView',
67 'TeamBreadcrumb',67 'TeamBreadcrumb',
68 'TeamEditLocationView',
69 'TeamEditMenu',68 'TeamEditMenu',
70 'TeamIndexMenu',69 'TeamIndexMenu',
71 'TeamJoinView',70 'TeamJoinView',
@@ -148,7 +147,6 @@
148 helpers,147 helpers,
149 )148 )
150from canonical.launchpad.browser.feeds import FeedsMixin149from canonical.launchpad.browser.feeds import FeedsMixin
151from canonical.launchpad.browser.launchpad import get_launchpad_views
152from canonical.launchpad.interfaces.account import (150from canonical.launchpad.interfaces.account import (
153 AccountStatus,151 AccountStatus,
154 IAccount,152 IAccount,
@@ -244,7 +242,6 @@
244from lp.code.browser.sourcepackagerecipelisting import HasRecipesMenuMixin242from lp.code.browser.sourcepackagerecipelisting import HasRecipesMenuMixin
245from lp.code.errors import InvalidNamespace243from lp.code.errors import InvalidNamespace
246from lp.code.interfaces.branchnamespace import IBranchNamespaceSet244from lp.code.interfaces.branchnamespace import IBranchNamespaceSet
247from lp.registry.browser import MapMixin
248from lp.registry.browser.branding import BrandingChangeView245from lp.registry.browser.branding import BrandingChangeView
249from lp.registry.browser.mailinglists import enabled_with_active_mailing_list246from lp.registry.browser.mailinglists import enabled_with_active_mailing_list
250from lp.registry.browser.menu import (247from lp.registry.browser.menu import (
@@ -3325,7 +3322,7 @@
3325 return self.state is EmailAddressVisibleState.ALLOWED3322 return self.state is EmailAddressVisibleState.ALLOWED
33263323
33273324
3328class PersonIndexView(XRDSContentNegotiationMixin, MapMixin, PersonView):3325class PersonIndexView(XRDSContentNegotiationMixin, PersonView):
3329 """View class for person +index and +xrds pages."""3326 """View class for person +index and +xrds pages."""
33303327
3331 xrds_template = ViewPageTemplateFile(3328 xrds_template = ViewPageTemplateFile(
@@ -3333,14 +3330,6 @@
33333330
3334 def initialize(self):3331 def initialize(self):
3335 super(PersonIndexView, self).initialize()3332 super(PersonIndexView, self).initialize()
3336 # This view requires the gmap2 Javascript in order to render the map
3337 # with the person's usual location. The location is only availble if
3338 # the location is set, visible, and the viewing user wants to see it.
3339 launchpad_views = get_launchpad_views(self.request.cookies)
3340 self._small_map = launchpad_views['small_maps']
3341 if (self.gmap2_enabled
3342 and self.has_visible_location and self._small_map):
3343 self.request.needs_gmap2 = True
3344 if self.request.method == "POST":3333 if self.request.method == "POST":
3345 self.processForm()3334 self.processForm()
33463335
@@ -3397,9 +3386,6 @@
3397 assert self.has_visible_location, (3386 assert self.has_visible_location, (
3398 "Can't generate the map for a person who hasn't set a "3387 "Can't generate the map for a person who hasn't set a "
3399 "visible location.")3388 "visible location.")
3400 assert self.request.needs_gmap2 or not self._small_map, (
3401 "To use this method a view must flag that it needs gmap2.")
3402
3403 replacements = {'center_lat': self.context.latitude,3389 replacements = {'center_lat': self.context.latitude,
3404 'center_lng': self.context.longitude}3390 'center_lng': self.context.longitude}
3405 return u"""3391 return u"""
@@ -5638,7 +5624,7 @@
5638class PersonLocationForm(Interface):5624class PersonLocationForm(Interface):
56395625
5640 location = LocationField(5626 location = LocationField(
5641 title=_('Use the map to indicate default location'),5627 title=_('Time zone'),
5642 required=True)5628 required=True)
5643 hide = Bool(5629 hide = Bool(
5644 title=_("Hide my location details from others."),5630 title=_("Hide my location details from others."),
@@ -5649,37 +5635,15 @@
5649 """Edit a person's location."""5635 """Edit a person's location."""
56505636
5651 schema = PersonLocationForm5637 schema = PersonLocationForm
5652 field_names = ['location', 'hide']5638 field_names = ['location']
5653 custom_widget('location', LocationWidget)5639 custom_widget('location', LocationWidget)
56545640 page_title = label = 'Set timezone'
5655 @property5641
5656 def page_title(self):5642 @property
5657 return smartquote(5643 def next_url(self):
5658 "%s's location and timezone" % self.context.displayname)5644 return canonical_url(self.context)
56595645
5660 label = page_title5646 cancel_url = next_url
5661
5662 @property
5663 def initial_values(self):
5664 """See `LaunchpadFormView`.
5665
5666 Set the initial value for the 'hide' field. The initial value for the
5667 'location' field is set by its widget.
5668 """
5669 if self.context.location is None:
5670 return {}
5671 else:
5672 return {'hide': not self.context.location.visible}
5673
5674 def initialize(self):
5675 self.next_url = canonical_url(self.context)
5676 self.for_team_name = self.request.form.get('for_team')
5677 if self.for_team_name is not None:
5678 for_team = getUtility(IPersonSet).getByName(self.for_team_name)
5679 if for_team is not None:
5680 self.next_url = canonical_url(for_team) + '/+map'
5681 super(PersonEditLocationView, self).initialize()
5682 self.cancel_url = self.next_url
56835647
5684 @action(_("Update"), name="update")5648 @action(_("Update"), name="update")
5685 def action_update(self, action, data):5649 def action_update(self, action, data):
@@ -5696,17 +5660,6 @@
5696 self.context.setLocationVisibility(visible)5660 self.context.setLocationVisibility(visible)
56975661
56985662
5699class TeamEditLocationView(LaunchpadView):
5700 """Redirect to the team's +map page.
5701
5702 We do that because it doesn't make sense to specify the location of a
5703 team."""
5704
5705 def initialize(self):
5706 self.request.response.redirect(
5707 canonical_url(self.context, view_name="+map"))
5708
5709
5710def archive_to_person(archive):5663def archive_to_person(archive):
5711 """Adapts an `IArchive` to an `IPerson`."""5664 """Adapts an `IArchive` to an `IPerson`."""
5712 return IPerson(archive.owner)5665 return IPerson(archive.owner)
57135666
=== modified file 'lib/lp/registry/browser/team.py'
--- lib/lp/registry/browser/team.py 2010-09-19 03:13:01 +0000
+++ lib/lp/registry/browser/team.py 2010-09-22 06:47:48 +0000
@@ -25,10 +25,7 @@
2525
26from datetime import datetime26from datetime import datetime
27import math27import math
28from urllib import (28from urllib import unquote
29 quote,
30 unquote,
31 )
3229
33import pytz30import pytz
34from zope.app.form.browser import TextAreaWidget31from zope.app.form.browser import TextAreaWidget
@@ -71,7 +68,6 @@
71 LaunchpadRadioWidget,68 LaunchpadRadioWidget,
72 )69 )
73from lp.app.errors import UnexpectedFormData70from lp.app.errors import UnexpectedFormData
74from lp.registry.browser import MapMixin
75from lp.registry.browser.branding import BrandingChangeView71from lp.registry.browser.branding import BrandingChangeView
76from lp.registry.interfaces.mailinglist import (72from lp.registry.interfaces.mailinglist import (
77 IMailingList,73 IMailingList,
@@ -1083,7 +1079,7 @@
1083 self.request.response.addInfoNotification(msg)1079 self.request.response.addInfoNotification(msg)
10841080
10851081
1086class TeamMapView(MapMixin, LaunchpadView):1082class TeamMapView(LaunchpadView):
1087 """Show all people with known locations on a map.1083 """Show all people with known locations on a map.
10881084
1089 Also provides links to edit the locations of people in the team without1085 Also provides links to edit the locations of people in the team without
@@ -1093,12 +1089,6 @@
1093 label = "Team member locations"1089 label = "Team member locations"
1094 limit = None1090 limit = None
10951091
1096 def initialize(self):
1097 # Tell our base-layout to include Google's gmap2 javascript so that
1098 # we can render the map.
1099 if self.gmap2_enabled and self.mapped_participants_count > 0:
1100 self.request.needs_gmap2 = True
1101
1102 @cachedproperty1092 @cachedproperty
1103 def mapped_participants(self):1093 def mapped_participants(self):
1104 """Participants with locations."""1094 """Participants with locations."""
11051095
=== modified file 'lib/lp/registry/browser/tests/person-views.txt'
--- lib/lp/registry/browser/tests/person-views.txt 2010-08-28 23:01:18 +0000
+++ lib/lp/registry/browser/tests/person-views.txt 2010-09-22 06:47:48 +0000
@@ -301,113 +301,6 @@
301 Portuguese (Brazil)301 Portuguese (Brazil)
302302
303303
304Location
305--------
306
307The Person profile page contains the location portlet that shows a map.
308The map requires the google GMap JavaScript to display, so the views set
309the state of the request's needs_gmap2 attribute to True only when the
310user has set his latitude, it is visible, and the viewing user wishes to
311see it. The map is not rendered if the user has not set his location.
312
313 >>> sample_person.latitude is None
314 True
315
316 >>> person_view = create_initialized_view(sample_person, '+index')
317 >>> person_view.request.needs_gmap2
318 False
319
320The map_portlet_html property that creates the map cannot be called.
321
322 >>> print person_view.map_portlet_html
323 Traceback (most recent call last):
324 ...
325 AssertionError: Can't generate the map for a person who hasn't set
326 a visible location.
327
328If the user sets his location, but does not make it visible, needs_gmap2
329will still be False and the map_portlet_html property cannot be called.
330
331 >>> login_person(sample_person)
332 >>> sample_person.setLocation(
333 ... 38.81, 77.1, 'America/New_York', sample_person)
334 >>> sample_person.setLocationVisibility(False)
335 >>> login('no-priv@canonical.com')
336
337 >>> person_view = create_initialized_view(sample_person, '+index')
338 >>> person_view.request.needs_gmap2
339 False
340
341 >>> print person_view.map_portlet_html
342 Traceback (most recent call last):
343 ...
344 AssertionError: Can't generate the map for a person who hasn't set
345 a visible location.
346
347When the user set's his visibility to True, needs_gmap2 will be true and
348the map_portlet_html can be called.
349
350 >>> from lp.services.features.model import FeatureFlag, getFeatureStore
351 >>> ignore = getFeatureStore().add(FeatureFlag(
352 ... scope=u'default', flag=u'gmap2', value=u'on', priority=1))
353 >>> transaction.commit()
354
355 >>> login_person(sample_person)
356 >>> sample_person.setLocationVisibility(True)
357
358 >>> person_view = create_initialized_view(sample_person, '+index')
359 >>> person_view.request.needs_gmap2
360 True
361
362 >>> print person_view.map_portlet_html
363 <script type="text/javascript">
364 YUI().use('node', 'lp.app.mapping', function(Y) { ...
365
366The small_maps key in the launchpad_views cookie can be set of the
367viewing user to 'false' to indicate that small maps are not wanted.
368While needs_gmap2 is False, the map_portlet_html property's markup is
369still needed to render the 'Show maps' checkbox.
370
371 >>> cookie = 'launchpad_views=small_maps=false'
372 >>> person_view = create_initialized_view(
373 ... sample_person, '+index', cookie=cookie)
374 >>> person_view.request.needs_gmap2
375 False
376
377 >>> print person_view.map_portlet_html
378 <script type="text/javascript">
379 YUI().use('node', 'lp.app.mapping', function(Y) { ...
380
381The map portlet is shown if the user has not set his location and is
382viewing his own page.
383
384 >>> user = factory.makePerson()
385 >>> user.latitude is None
386 True
387
388 >>> login_person(user)
389 >>> person_view = create_initialized_view(
390 ... user, '+index')
391 >>> person_view.should_show_map_portlet
392 True
393
394However another user will not be shown the portlet.
395
396 >>> login('foo.bar@canonical.com')
397 >>> person_view = create_initialized_view(
398 ... user, '+index')
399 >>> person_view.should_show_map_portlet
400 False
401
402If a user has a location set and it is visibible then the portlet is
403shown.
404
405 >>> person_view = create_initialized_view(
406 ... sample_person, '+index')
407 >>> person_view.should_show_map_portlet
408 True
409
410
411Things a person is working on304Things a person is working on
412-----------------------------305-----------------------------
413306
414307
=== modified file 'lib/lp/registry/browser/tests/team-views.txt'
--- lib/lp/registry/browser/tests/team-views.txt 2010-08-28 23:01:18 +0000
+++ lib/lp/registry/browser/tests/team-views.txt 2010-09-22 06:47:48 +0000
@@ -67,21 +67,11 @@
6767
68== +map-portlet ==68== +map-portlet ==
6969
70The team profile page contain the location portlet that shows a map. The70The team profile page contain the location portlet that shows a map.
71map requires the google GMap JavaScript to display, so the views set the
72state of the request's needs_gmap2 attribute to true if there are
73members who have set their location.
74
75 >>> from lp.services.features.model import FeatureFlag, getFeatureStore
76 >>> ignore = getFeatureStore().add(FeatureFlag(
77 ... scope=u'default', flag=u'gmap2', value=u'on', priority=1))
78 >>> transaction.commit()
7971
80 >>> team_view = create_initialized_view(ubuntu_team, '+index')72 >>> team_view = create_initialized_view(ubuntu_team, '+index')
81 >>> team_view.has_visible_location73 >>> team_view.has_visible_location
82 False74 False
83 >>> team_view.request.needs_gmap2
84 False
8575
86After a member has set his location, the map will be rendered.76After a member has set his location, the map will be rendered.
8777
@@ -92,8 +82,6 @@
92 >>> team_view = create_initialized_view(ubuntu_team, '+index')82 >>> team_view = create_initialized_view(ubuntu_team, '+index')
93 >>> team_view.has_visible_location83 >>> team_view.has_visible_location
94 True84 True
95 >>> team_view.request.needs_gmap2
96 True
9785
98The small_maps key in the launchpad_views cookie can be set by the viewing86The small_maps key in the launchpad_views cookie can be set by the viewing
99user to 'false' to indicate that small maps are not wanted.87user to 'false' to indicate that small maps are not wanted.
@@ -101,9 +89,6 @@
101 >>> cookie = 'launchpad_views=small_maps=false'89 >>> cookie = 'launchpad_views=small_maps=false'
102 >>> team_view = create_initialized_view(90 >>> team_view = create_initialized_view(
103 ... ubuntu_team, '+index', cookie=cookie)91 ... ubuntu_team, '+index', cookie=cookie)
104 >>> team_view.request.needs_gmap2
105 False
106
10792
108== +map ==93== +map ==
10994
@@ -118,15 +103,10 @@
118 >>> view.times103 >>> view.times
119 []104 []
120105
121There are no mapped member yet, so needs_gmap2 is False, so the map will106
122not be rendered.107Once a member is mapped, the map will be rendered. The view provides a cached
123108property to access the mapped participants. The views number of times is
124 >>> view.request.needs_gmap2109incremented for each timezone the members reside in.
125 False
126
127Once a member is mapped, needs_gmap2 is True and the map will be rendered.
128The view provides a cached property to access the mapped participants. The
129views number of times is incremented for each timezone the members reside in.
130110
131 >>> london_member = factory.makePerson(111 >>> london_member = factory.makePerson(
132 ... latitude=51.49, longitude=-0.13, time_zone='Europe/London')112 ... latitude=51.49, longitude=-0.13, time_zone='Europe/London')
@@ -139,9 +119,6 @@
139 >>> len(view.times)119 >>> len(view.times)
140 1120 1
141121
142 >>> view.request.needs_gmap2
143 True
144
145 >>> brazil_member = factory.makePerson(122 >>> brazil_member = factory.makePerson(
146 ... latitude=-23.60, longitude=-46.64, time_zone='America/Sao_Paulo')123 ... latitude=-23.60, longitude=-46.64, time_zone='America/Sao_Paulo')
147 >>> ignored = context.addMember(brazil_member, mark)124 >>> ignored = context.addMember(brazil_member, mark)
148125
=== modified file 'lib/lp/registry/doc/personlocation.txt'
--- lib/lp/registry/doc/personlocation.txt 2010-08-20 12:25:28 +0000
+++ lib/lp/registry/doc/personlocation.txt 2010-09-22 06:47:48 +0000
@@ -1,4 +1,5 @@
1= Locations for People and Teams =1Locations for People and Teams
2==============================
23
3The PersonLocation object stores information about the location and time4The PersonLocation object stores information about the location and time
4zone of a person. It also remembers who provided that information, and5zone of a person. It also remembers who provided that information, and
@@ -164,7 +165,8 @@
164 mapped_participants_count == 0.165 mapped_participants_count == 0.
165166
166167
167== Location visibility ==168Location visibility
169-------------------
168170
169Some people may not want their location to be disclosed to others, so171Some people may not want their location to be disclosed to others, so
170we provide a way for them to hide their location from other users. By172we provide a way for them to hide their location from other users. By
171173
=== modified file 'lib/lp/registry/stories/location/personlocation-edit.txt'
--- lib/lp/registry/stories/location/personlocation-edit.txt 2009-11-15 01:05:49 +0000
+++ lib/lp/registry/stories/location/personlocation-edit.txt 2010-09-22 06:47:48 +0000
@@ -1,4 +1,5 @@
1== Edit person location information ==1Edit person location information
2================================
23
3A person's location is only editable by people who have launchpad.Edit on4A person's location is only editable by people who have launchpad.Edit on
4the person, which is that person and admins.5the person, which is that person and admins.
@@ -49,35 +50,3 @@
49 >>> admin_browser.open('http://launchpad.dev/~zzz/+editlocation')50 >>> admin_browser.open('http://launchpad.dev/~zzz/+editlocation')
50 >>> admin_browser.getControl(name='field.location.latitude').value51 >>> admin_browser.getControl(name='field.location.latitude').value
51 '39.48'52 '39.48'
52
53The +editlocation page also allows a person to change his location
54visibility, that is, whether or not others can see it.
55
56 >>> nopriv_browser.open('http://launchpad.dev/~no-priv/+editlocation')
57 >>> nopriv_browser.getControl(
58 ... 'Hide my location details from others.').selected = True
59 >>> nopriv_browser.getControl('Update').click()
60 >>> nopriv_browser.url
61 'http://launchpad.dev/~no-priv'
62
63Once hidden, other users can't see it.
64
65 >>> name12_browser = setupBrowser(auth="Basic test@canonical.com:test")
66 >>> name12_browser.open('http://launchpad.dev/~no-priv')
67 >>> print str(find_tag_by_id(name12_browser.contents, 'person_map_div'))
68 None
69
70The person himself can still see and change it, though.
71
72 >>> nopriv_browser.open('http://launchpad.dev/~no-priv')
73 >>> print str(find_tag_by_id(nopriv_browser.contents, 'portlet-map'))
74 <div...
75 <h2>Location</h2>
76 ...
77
78 >>> nopriv_browser.open('http://launchpad.dev/~no-priv/+editlocation')
79 >>> nopriv_browser.getControl(
80 ... 'Hide my location details from others.').selected = False
81 >>> nopriv_browser.getControl('Update').click()
82 >>> nopriv_browser.url
83 'http://launchpad.dev/~no-priv'
8453
=== modified file 'lib/lp/registry/stories/location/personlocation.txt'
--- lib/lp/registry/stories/location/personlocation.txt 2010-08-27 22:42:17 +0000
+++ lib/lp/registry/stories/location/personlocation.txt 2010-09-22 06:47:48 +0000
@@ -16,43 +16,3 @@
16 >>> anon_browser.open('http://launchpad.dev/~zzz')16 >>> anon_browser.open('http://launchpad.dev/~zzz')
17 >>> print extract_text(17 >>> print extract_text(
18 ... find_tag_by_id(anon_browser.contents, 'portlet-map'))18 ... find_tag_by_id(anon_browser.contents, 'portlet-map'))
19
20If a person has a location, but the gmap2 feature is not enabled, the user
21sees the timezone, but no map.
22
23 >>> login('test@canonical.com')
24 >>> yyy = factory.makePerson(name='yyy', time_zone='Europe/London',
25 ... latitude=52.2, longitude=0.3)
26 >>> logout()
27
28 >>> anon_browser.open('http://launchpad.dev/~yyy')
29 >>> markup = str(anon_browser.contents)
30 >>> print extract_text(
31 ... find_tag_by_id(markup, 'portlet-map'), skip_tags=[])
32 Location
33 Time zone: Europe/London...
34
35 >>> 'src="http://maps.google.com/maps' in markup
36 False
37
38If a person has a location, there is a little map portlet in their
39profile page. We can't test all the google javascript, but we can make sure
40there's a map, and the scripts are loaded when the gmap2 feature is enabled
41for users.
42
43 >>> from lp.services.features.model import FeatureFlag, getFeatureStore
44 >>> ignore = getFeatureStore().add(FeatureFlag(
45 ... scope=u'default', flag=u'gmap2', value=u'on', priority=1))
46 >>> transaction.commit()
47
48 >>> anon_browser.open('http://launchpad.dev/~yyy')
49 >>> markup = str(anon_browser.contents)
50 >>> print extract_text(
51 ... find_tag_by_id(markup, 'portlet-map'), skip_tags=[])
52 Location
53 Time zone: Europe/London...
54 Y.lp.app.mapping.renderPersonMapSmall(...
55 >>> 'src="http://maps.google.com/maps' in markup
56 True
57 >>> 'build/app/mapping.js' in markup
58 True
5919
=== modified file 'lib/lp/registry/stories/location/team-map.txt'
--- lib/lp/registry/stories/location/team-map.txt 2010-08-27 22:33:36 +0000
+++ lib/lp/registry/stories/location/team-map.txt 2010-09-22 06:47:48 +0000
@@ -1,50 +1,6 @@
1The map of a team's members1The map of a team's members
2===========================2===========================
33
4Maps are disabled
5-----------------
6
7Users cannot see maps when the gmap2 feature is disbaled for them
8
9 >>> user_browser.open('http://launchpad.dev/~guadamen')
10 >>> body = find_main_content(user_browser.contents)
11 >>> mapdiv = find_tag_by_id(str(body), 'team_map_div')
12 >>> 'lp.app.mapping.renderTeamMapSmall(' in str(body)
13 False
14
15
16Maps are enabled
17----------------
18
19Users can see maps when the gmap2 feature is enabled for them.
20
21 >>> from lp.services.features.model import FeatureFlag, getFeatureStore
22 >>> ignore = getFeatureStore().add(FeatureFlag(
23 ... scope=u'default', flag=u'gmap2', value=u'on', priority=1))
24 >>> transaction.commit()
25
26If a team has members that have locations, then you should see a portlet
27with their locations displayed.
28
29 >>> nopriv_browser = setupBrowser(auth='Basic no-priv@canonical.com:test')
30 >>> nopriv_browser.open('http://launchpad.dev/~guadamen')
31 >>> body = find_main_content(nopriv_browser.contents)
32 >>> mapdiv = find_tag_by_id(str(body), 'team_map_div')
33 >>> 'lp.app.mapping.renderTeamMapSmall(' in str(body)
34 True
35 >>> markup = str(nopriv_browser.contents)
36 >>> 'src="http://maps.google.com/maps' in markup
37 True
38 >>> 'build/app/mapping.js' in markup
39 True
40
41You should also be able to see a map of the team.
42
43 >>> maplink = nopriv_browser.getLink('View map and time zones')
44 >>> maplink.click()
45 >>> nopriv_browser.url
46 'http://launchpad.dev/~guadamen/+map'
47
48The map depends on a stream of XML-formatted data, giving the locations of4The map depends on a stream of XML-formatted data, giving the locations of
49all members of the team. We show that this stream works for teams with, and5all members of the team. We show that this stream works for teams with, and
50without, mapped members.6without, mapped members.
@@ -108,15 +64,6 @@
108 <BLANKLINE>64 <BLANKLINE>
10965
11066
111It doesn't make sense to edit the location of the team itself, not even
112if we are an admin, so a team's +editlocation page will simply redirect
113to +map.
114
115 >>> admin_browser.open('http://launchpad.dev/~guadamen/+editlocation')
116 >>> print admin_browser.url
117 http://launchpad.dev/~guadamen/+map
118
119
120+mapdata67+mapdata
121--------68--------
12269
12370
=== removed file 'lib/lp/registry/templates/person-editlocation.pt'
--- lib/lp/registry/templates/person-editlocation.pt 2009-09-01 19:34:46 +0000
+++ lib/lp/registry/templates/person-editlocation.pt 1970-01-01 00:00:00 +0000
@@ -1,23 +0,0 @@
1<html
2 xmlns="http://www.w3.org/1999/xhtml"
3 xmlns:tal="http://xml.zope.org/namespaces/tal"
4 xmlns:metal="http://xml.zope.org/namespaces/metal"
5 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
6 metal:use-macro="view/macro:page/main_only"
7 i18n:domain="launchpad">
8
9<body>
10
11 <div metal:fill-slot="main">
12
13 <div metal:use-macro="context/@@launchpad_form/form">
14 <div metal:fill-slot="extra_info">
15 <input type="hidden" name="for_team" value=""
16 tal:attributes="value view/for_team_name" />
17 </div>
18 </div>
19
20 </div>
21
22</body>
23</html>
240
=== modified file 'lib/lp/registry/templates/person-portlet-map.pt'
--- lib/lp/registry/templates/person-portlet-map.pt 2010-08-27 22:33:36 +0000
+++ lib/lp/registry/templates/person-portlet-map.pt 2010-09-22 06:47:48 +0000
@@ -8,7 +8,6 @@
8 tal:define="overview_menu context/menu:overview">8 tal:define="overview_menu context/menu:overview">
99
10 <tal:show-map condition="view/should_show_map_portlet">10 <tal:show-map condition="view/should_show_map_portlet">
11
12 <h2>Location</h2>11 <h2>Location</h2>
1312
14 <div tal:condition="context/time_zone">13 <div tal:condition="context/time_zone">
@@ -16,30 +15,6 @@
16 <span tal:replace="context/time_zone">UTC</span>15 <span tal:replace="context/time_zone">UTC</span>
17 <a tal:replace="structure overview_menu/editlocation/fmt:icon" />16 <a tal:replace="structure overview_menu/editlocation/fmt:icon" />
18 </div>17 </div>
19
20
21 <tal:gmap2 condition="view/gmap2_enabled">
22 <div style="width: 400px;" tal:condition="context/latitude">
23 <div id="person_map_actions"
24 style="position:relative; z-index: 9999;
25 float:right; width: 8.5em; margin: 2px;
26 background-color: white; padding-bottom:1px;"></div>
27 <div id="person_map_div" class="small-map"
28 style="height: 200px; border: 1px; margin-top: 4px;"></div>
29 <tal:mapscript replace="structure view/map_portlet_html" />
30 <div style="margin-top: 0px;">
31 <a tal:replace="structure overview_menu/editlocation/fmt:link-icon" />
32 </div>
33 </div>
34 </tal:gmap2>
35
36 <tal:comment condition="nothing">
37 Only the user can see the editlocation image and link.
38 </tal:comment>
39 <a tal:condition="not: context/latitude"
40 tal:attributes="href overview_menu/editlocation/target"
41 ><img src="/+icing/portlet-map-unknown.png" />
42 </a>
43 </tal:show-map>18 </tal:show-map>
4419
45</div>20</div>
4621
=== modified file 'lib/lp/registry/templates/team-index.pt'
--- lib/lp/registry/templates/team-index.pt 2010-06-28 21:56:49 +0000
+++ lib/lp/registry/templates/team-index.pt 2010-09-22 06:47:48 +0000
@@ -88,9 +88,6 @@
88 <metal:subteam-of use-macro="context/@@+person-macros/subteam-of" />88 <metal:subteam-of use-macro="context/@@+person-macros/subteam-of" />
89 </div>89 </div>
90 </div>90 </div>
91
92 <div tal:content="structure context/@@+portlet-map" />
93
94</div>91</div>
95</body>92</body>
96</html>93</html>
9794
=== modified file 'lib/lp/registry/templates/team-portlet-map.pt'
--- lib/lp/registry/templates/team-portlet-map.pt 2010-08-27 22:33:36 +0000
+++ lib/lp/registry/templates/team-portlet-map.pt 2010-09-22 06:47:48 +0000
@@ -5,8 +5,7 @@
5 omit-tag="">5 omit-tag="">
66
7<div class="portlet" id="portlet-map" style="margin-bottom: 0px;"7<div class="portlet" id="portlet-map" style="margin-bottom: 0px;"
8 tal:define="link context/menu:overview/map"8 tal:define="link context/menu:overview/map">
9 tal:condition="view/gmap2_enabled">
10<table>9<table>
11<tr><td>10<tr><td>
12 <h2>11 <h2>
1312
=== modified file 'test_on_merge.py'
--- test_on_merge.py 2010-08-10 21:27:56 +0000
+++ test_on_merge.py 2010-09-22 06:47:48 +0000
@@ -1,6 +1,6 @@
1#!/usr/bin/python -S1#!/usr/bin/python -S
2#2#
3# Copyright 2009 Canonical Ltd. This software is licensed under the3# Copyright 2009, 2010 Canonical Ltd. This software is licensed under the
4# GNU Affero General Public License version 3 (see the file LICENSE).4# GNU Affero General Public License version 3 (see the file LICENSE).
55
6"""Tests that get run automatically on a merge."""6"""Tests that get run automatically on a merge."""
@@ -13,7 +13,7 @@
13import psycopg213import psycopg2
14from subprocess import Popen, PIPE, STDOUT14from subprocess import Popen, PIPE, STDOUT
15from signal import SIGKILL, SIGTERM, SIGINT, SIGHUP15from signal import SIGKILL, SIGTERM, SIGINT, SIGHUP
16from select import select16import select
1717
1818
19# The TIMEOUT setting (expressed in seconds) affects how long a test will run19# The TIMEOUT setting (expressed in seconds) affects how long a test will run
@@ -164,7 +164,22 @@
164 # Popen.communicate() with large data sets.164 # Popen.communicate() with large data sets.
165 open_readers = set([xvfb_proc.stdout])165 open_readers = set([xvfb_proc.stdout])
166 while open_readers:166 while open_readers:
167 rlist, wlist, xlist = select(open_readers, [], [], TIMEOUT)167 # blocks for a long time and can easily fail with EINTR
168 # <https://bugs.launchpad.net/launchpad/+bug/615740> - catching
169 # it just here is not the perfect fix (other syscalls might be
170 # interrupted) but is pragmatic
171 while True:
172 try:
173 rlist, wlist, xlist = select.select(open_readers, [], [], TIMEOUT)
174 break
175 except select.error, e:
176 # nb: select.error doesn't expose a named 'errno' attribute,
177 # at least in python 2.6.5; see
178 # <http://mail.python.org/pipermail/python-dev/2000-October/009671.html>
179 if e[0] == errno.EINTR:
180 continue
181 else:
182 raise
168183
169 if len(rlist) == 0:184 if len(rlist) == 0:
170 # The select() statement timed out!185 # The select() statement timed out!

Subscribers

People subscribed via source and target branches

to status/vote changes: