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