Merge lp:~salgado/launchpad/remove-map-views into lp:launchpad

Proposed by Guilherme Salgado
Status: Merged
Approved by: Ian Booth
Approved revision: no longer in the source branch.
Merged at revision: 14834
Proposed branch: lp:~salgado/launchpad/remove-map-views
Merge into: lp:launchpad
Diff against target: 1945 lines (+95/-1416)
26 files modified
lib/lp/app/widgets/doc/location-widget.txt (+0/-204)
lib/lp/app/widgets/location.py (+0/-192)
lib/lp/app/widgets/templates/location.pt (+0/-2)
lib/lp/blueprints/browser/tests/sprintattendance-views.txt (+0/-20)
lib/lp/registry/browser/configure.zcml (+1/-28)
lib/lp/registry/browser/person.py (+21/-71)
lib/lp/registry/browser/team.py (+0/-116)
lib/lp/registry/browser/tests/team-views.txt (+0/-129)
lib/lp/registry/doc/personlocation.txt (+17/-195)
lib/lp/registry/interfaces/location.py (+12/-6)
lib/lp/registry/interfaces/person.py (+1/-35)
lib/lp/registry/model/person.py (+18/-113)
lib/lp/registry/stories/location/personlocation-edit.txt (+7/-28)
lib/lp/registry/stories/location/personlocation.txt (+0/-18)
lib/lp/registry/stories/location/team-map.txt (+0/-86)
lib/lp/registry/stories/person/xx-person-home.txt (+5/-20)
lib/lp/registry/stories/webservice/xx-personlocation.txt (+9/-25)
lib/lp/registry/templates/person-index.pt (+0/-2)
lib/lp/registry/templates/person-portlet-contact-details.pt (+1/-1)
lib/lp/registry/templates/person-portlet-map.pt (+0/-21)
lib/lp/registry/templates/team-map-data.pt (+0/-17)
lib/lp/registry/templates/team-map.pt (+0/-45)
lib/lp/registry/templates/team-portlet-map.pt (+0/-37)
lib/lp/registry/tests/test_person.py (+1/-2)
lib/lp/registry/tests/test_personset.py (+1/-2)
lib/lp/registry/xmlrpc/canonicalsso.py (+1/-1)
To merge this branch: bzr merge lp:~salgado/launchpad/remove-map-views
Reviewer Review Type Date Requested Status
Ian Booth (community) Approve
Review via email: mp+93509@code.launchpad.net

Commit message

[r=wallyworld][bug=933911] Remove most of the code related to people's locations. We're not able to remove everything yet because the time zone is still stored in PersonLocation.

Description of the change

This removes most of the code related to people's locations. We're not able to remove everything yet because the time zone is still stored in PersonLocation, but I've filed a bug for that.

As per IRC discussion with Rob, I've unexported the location-related bits on the devel API and made them always return None for other versions. I wanted to get rid of Person.location and Person.setLocation() but that would have performance implications (or so some tests tell me) so I left them in even though the former is only used to get the time zone and the latter only used to set it.

To post a comment you must log in.
Revision history for this message
Ian Booth (wallyworld) wrote :

This looks ok to land. Please file a bug and link the branch to it so that it can be qa'ed.

review: Approve
Revision history for this message
Guilherme Salgado (salgado) wrote :

Thanks for the review, Ian. I've linked a bug to my branch

Revision history for this message
Guilherme Salgado (salgado) wrote :

There was a test which was still using setLocationVisibility() so this didn't land. I've pushed a fix for it and now it should be good to go.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== removed file 'lib/lp/app/widgets/doc/location-widget.txt'
--- lib/lp/app/widgets/doc/location-widget.txt 2012-02-02 12:30:53 +0000
+++ lib/lp/app/widgets/doc/location-widget.txt 1970-01-01 00:00:00 +0000
@@ -1,204 +0,0 @@
1Location widget
2===============
3
4A widget used when setting the geographic location of a given person.
5
6 >>> from lp.services.webapp.servers import LaunchpadTestRequest
7 >>> from lp.app.widgets.location import LocationWidget
8 >>> from lp.registry.interfaces.person import IPersonSet
9 >>> from lp.services.fields import LocationField
10 >>> salgado = getUtility(IPersonSet).getByName('salgado')
11 >>> field = LocationField(__name__='location', title=u'Location')
12
13 >>> bound_field = field.bind(salgado)
14 >>> request = LaunchpadTestRequest(
15 ... # Let's pretend requests are coming from Brazil.
16 ... environ={'REMOTE_ADDR': '201.13.165.145'})
17
18When rendering salgado's location widget to himself, we'll center the
19map around the location where he seems to be, based on the IP address of
20the request.
21
22 >>> login_person(salgado)
23 >>> widget = LocationWidget(bound_field, request)
24 >>> widget.zoom
25 7
26 >>> widget.center_lat
27 -23...
28 >>> widget.center_lng
29 -45...
30
31Since salgado's location hasn't been specified yet, there'll be no
32visual marker indicating where he is in the map.
33
34 >>> widget.show_marker
35 0
36
37If someone else sees salgado's location widget, though, we have no way
38of guessing what coordinates to center the map around (because the
39request doesn't come from salgado's browser), so we won't try to do so.
40
41 >>> login(ANONYMOUS)
42 >>> widget = LocationWidget(bound_field, request)
43 >>> widget.zoom
44 2
45 >>> widget.center_lat
46 15.0
47 >>> widget.center_lng
48 0.0
49 >>> widget.show_marker
50 0
51
52When rendering the location widget of someone who already specified a
53location, we'll obviously center the map around that, but we'll also put
54a visual marker in the latitude/longitude specified as that person's
55location.
56
57 >>> kamion = getUtility(IPersonSet).getByName('kamion')
58 >>> bound_field = field.bind(kamion)
59 >>> login_person(kamion)
60 >>> widget = LocationWidget(bound_field, request)
61 >>> widget.zoom
62 9
63 >>> widget.center_lat
64 52...
65
66This next test fails in ec2 but passes locally. On ec2, the printed value
67was 0.2999999. The round() should have fixed that. Testing shows that
68round() is broken in Python 2.6 and works in 2.7
69We'll disable this for now and fix in a cleanup branch.
70
71round(widget.center_lng, 5)
720.3...
73
74 >>> widget.show_marker
75 1
76
77The widget's getInputValue() method will return a LocationValue object,
78which stored the geographic coordinates and the timezone.
79
80 >>> request = LaunchpadTestRequest(
81 ... form={'field.location.time_zone': 'Europe/London',
82 ... 'field.location.latitude': '51.3',
83 ... 'field.location.longitude': '0.32'})
84 >>> widget = LocationWidget(bound_field, request)
85 >>> widget.hasInput()
86 True
87 >>> location = widget.getInputValue()
88 >>> location
89 <lp.app.widgets.location.LocationValue...
90 >>> location.latitude # Only the integral part due to rounding errors.
91 51...
92 >>> location.longitude
93 0.32...
94 >>> location.time_zone
95 'Europe/London'
96
97If we try to set only the latitude *or* longitude, but not both, we'll
98get an error.
99
100 >>> request = LaunchpadTestRequest(
101 ... form={'field.location.time_zone': 'Europe/London',
102 ... 'field.location.longitude': '0.32'})
103 >>> widget = LocationWidget(bound_field, request)
104 >>> widget.hasInput()
105 True
106 >>> widget.getInputValue()
107 Traceback (most recent call last):
108 ...
109 WidgetInputError:...Please provide both latitude and longitude...
110
111We also get errors if we don't specify a time zone or if the
112latitude/longitude have non-realistic values.
113
114 >>> request = LaunchpadTestRequest(
115 ... form={'field.location.latitude': '51.3',
116 ... 'field.location.longitude': '0.32'})
117 >>> widget = LocationWidget(bound_field, request)
118 >>> widget.hasInput()
119 True
120 >>> location = widget.getInputValue()
121 Traceback (most recent call last):
122 ...
123 MissingInputError: ('field.location.time_zone'...
124
125 >>> request = LaunchpadTestRequest(
126 ... form={'field.location.time_zone': 'Europe/London',
127 ... 'field.location.latitude': '99.3',
128 ... 'field.location.longitude': '0.32'})
129 >>> widget = LocationWidget(bound_field, request)
130 >>> widget.hasInput()
131 True
132 >>> widget.getInputValue()
133 Traceback (most recent call last):
134 ...
135 WidgetInputError:...Please provide a more realistic latitude and
136 longitude...
137
138
139The widget's HTML
140-----------------
141
142The widget's HTML will include <input> elements for the latitude,
143longitude and time zone fields. The values of these elements will be
144set from within Javascript whenever the user changes the location on
145the map.
146
147 >>> bound_field = field.bind(kamion)
148 >>> request = LaunchpadTestRequest(
149 ... environ={'REMOTE_ADDR': '201.13.165.145'})
150 >>> login_person(kamion)
151 >>> widget = LocationWidget(bound_field, request)
152 >>> print widget()
153 <input...name="field.location.latitude"...type="hidden"...
154 <input...name="field.location.longitude"...type="hidden"...
155 <select...name="field.location.time_zone"...
156
157
158The widget's script
159-------------------
160
161The widget initializes the mapping.renderPersonMap() JavaScript methods
162with its map_javascript property. The widget uses the
163mapping.setLocation() method to lookup the time zone for the location.
164The launchpad.geonames_identity config key provides the identity required to
165access ba-ws.geonames.net's service.
166
167 >>> from lp.services.config import config
168
169 >>> config.push('geoname_data', """
170 ... [launchpad]
171 ... geonames_identity: totoro
172 ... """)
173 >>> print widget.map_javascript
174 <BLANKLINE>
175 <script type="text/javascript">
176 LPJS.use('node', 'lp.app.mapping', function(Y) {
177 function renderMap() {
178 Y.lp.app.mapping.renderPersonMap(
179 52.2, 0.3, "Colin Watson",
180 'kamion', '<img ... />', 'totoro',
181 'field.location.latitude', 'field.location.longitude',
182 'field.location.time_zone', 9, 1);
183 }
184 Y.on("domready", renderMap);
185 });
186 </script>
187
188 # Restore the config the the default state.
189 >>> config_data = config.pop('geoname_data')
190
191
192XSS
193---
194
195The widget must escape and JS encode the person's displayname to prevent
196XSS attacks and to make sure the generated javascript can be parsed.
197
198 >>> kamion.displayname = (
199 ... "<script>alert('John \"nasty\" O\'Brien');</script>")
200 >>> bound_field = field.bind(kamion)
201 >>> widget = LocationWidget(bound_field, request)
202 >>> print widget.map_javascript
203 <BLANKLINE>
204 ... "&lt;script&gt;alert('John &quot;nasty&quot; O'Brien');&lt;/script&gt;",...
2050
=== removed file 'lib/lp/app/widgets/location.py'
--- lib/lp/app/widgets/location.py 2012-02-01 15:46:43 +0000
+++ lib/lp/app/widgets/location.py 1970-01-01 00:00:00 +0000
@@ -1,192 +0,0 @@
1# Copyright 2009-2010 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4# pylint: disable-msg=E0702
5
6__metaclass__ = type
7__all__ = [
8 'ILocationWidget',
9 'LocationWidget',
10 ]
11
12from lazr.restful.utils import safe_js_escape
13from z3c.ptcompat import ViewPageTemplateFile
14from zope.app.form import InputWidget
15from zope.app.form.browser.interfaces import IBrowserWidget
16from zope.app.form.browser.widget import BrowserWidget
17from zope.app.form.interfaces import (
18 IInputWidget,
19 WidgetInputError,
20 )
21from zope.component import getUtility
22from zope.formlib import form
23from zope.interface import implements
24from zope.schema import (
25 Choice,
26 Float,
27 )
28
29from lp import _
30from lp.app.browser.tales import ObjectImageDisplayAPI
31from lp.app.validators import LaunchpadValidationError
32from lp.registry.interfaces.location import IObjectWithLocation
33from lp.services.config import config
34from lp.services.geoip.interfaces import IGeoIPRecord
35from lp.services.webapp.interfaces import (
36 ILaunchBag,
37 IMultiLineWidgetLayout,
38 )
39
40
41class ILocationWidget(IInputWidget, IBrowserWidget, IMultiLineWidgetLayout):
42 """A widget for selecting a location and time zone."""
43
44
45class LocationValue:
46 """A location passed back from a LocationWidget.
47
48 This is a single object which contains the latitude, longitude and time
49 zone of the location.
50 """
51
52 def __init__(self, latitude, longitude, time_zone):
53 self.latitude = latitude
54 self.longitude = longitude
55 self.time_zone = time_zone
56
57
58class LocationWidget(BrowserWidget, InputWidget):
59 """See `ILocationWidget`."""
60 implements(ILocationWidget)
61
62 __call__ = ViewPageTemplateFile("templates/location.pt")
63
64 def __init__(self, context, request):
65 super(LocationWidget, self).__init__(context, request)
66 fields = form.Fields(
67 Float(__name__='latitude', title=_('Latitude'), required=False),
68 Float(__name__='longitude', title=_('Longitude'), required=False),
69 Choice(
70 __name__='time_zone', vocabulary='TimezoneName',
71 title=_('Time zone'), required=True,
72 description=_(
73 'Once the time zone is correctly set, events '
74 'in Launchpad will be displayed in local time.')))
75 # This will be the initial zoom level and center of the map.
76 self.zoom = 2
77 self.center_lat = 15.0
78 self.center_lng = 0.0
79 # By default, we will not show a marker initially, because we are
80 # not absolutely certain of the location we are proposing. The
81 # variable is a number that will be passed to JavaScript and
82 # evaluated as a boolean.
83 self.show_marker = 0
84 data = {
85 'time_zone': None,
86 'latitude': None,
87 'longitude': None,
88 }
89 # If we are creating a record for ourselves, then we will default to
90 # a location GeoIP guessed, and a higher zoom.
91 if getUtility(ILaunchBag).user == context.context:
92 geo_request = IGeoIPRecord(request)
93 self.zoom = 7
94 self.center_lat = geo_request.latitude
95 self.center_lng = geo_request.longitude
96 data['time_zone'] = geo_request.time_zone
97 current_location = IObjectWithLocation(self.context.context)
98 if current_location.latitude is not None:
99 # We are updating a location.
100 data['latitude'] = current_location.latitude
101 data['longitude'] = current_location.longitude
102 self.center_lat = current_location.latitude
103 self.center_lng = current_location.longitude
104 self.zoom = 9
105 self.show_marker = 1
106 if current_location.time_zone is not None:
107 # We are updating a time zone.
108 data['time_zone'] = current_location.time_zone
109 self.initial_values = data
110 widgets = form.setUpWidgets(
111 fields, self.name, context, request, ignore_request=False,
112 data=data)
113 self.time_zone_widget = widgets['time_zone']
114 self.latitude_widget = widgets['latitude']
115 self.longitude_widget = widgets['longitude']
116
117 @property
118 def map_javascript(self):
119 """The Javascript code necessary to render the map."""
120 person = self.context.context
121 replacements = dict(
122 center_lat=self.center_lat,
123 center_lng=self.center_lng,
124 displayname=safe_js_escape(person.displayname),
125 name=person.name,
126 logo_html=ObjectImageDisplayAPI(person).logo(),
127 geoname=config.launchpad.geonames_identity,
128 lat_name=self.latitude_widget.name,
129 lng_name=self.longitude_widget.name,
130 tz_name=self.time_zone_widget.name,
131 zoom=self.zoom,
132 show_marker=self.show_marker)
133 return """
134 <script type="text/javascript">
135 LPJS.use('node', 'lp.app.mapping', function(Y) {
136 function renderMap() {
137 Y.lp.app.mapping.renderPersonMap(
138 %(center_lat)s, %(center_lng)s, %(displayname)s,
139 '%(name)s', '%(logo_html)s', '%(geoname)s',
140 '%(lat_name)s', '%(lng_name)s', '%(tz_name)s',
141 %(zoom)s, %(show_marker)s);
142 }
143 Y.on("domready", renderMap);
144 });
145 </script>
146 """ % replacements
147
148 def hasInput(self):
149 """See `IBrowserWidget`.
150
151 Return True if time zone or latitude widgets have input.
152 """
153 return (self.time_zone_widget.hasInput()
154 or self.latitude_widget.hasInput())
155
156 def getInputValue(self):
157 """See `IBrowserWidget`.
158
159 Return a `LocationValue` object containing the latitude, longitude and
160 time zone chosen.
161 """
162 self._error = None
163 time_zone = self.time_zone_widget.getInputValue()
164 latitude = None
165 longitude = None
166 if self.latitude_widget.hasInput():
167 latitude = self.latitude_widget.getInputValue()
168 if self.longitude_widget.hasInput():
169 longitude = self.longitude_widget.getInputValue()
170 if time_zone is None:
171 self._error = WidgetInputError(
172 self.name, self.label,
173 LaunchpadValidationError(
174 _('Please provide a valid time zone.')))
175 raise self._error
176 if ((latitude is None and longitude is not None)
177 or (latitude is not None and longitude is None)):
178 # We must receive both a latitude and a longitude.
179 self._error = WidgetInputError(
180 self.name, self.label,
181 LaunchpadValidationError(
182 _('Please provide both latitude and longitude.')))
183 raise self._error
184 if latitude is not None:
185 if abs(latitude) > 90 or abs(longitude) > 180:
186 # We need latitude and longitude within range.
187 self._error = WidgetInputError(
188 self.name, self.label, LaunchpadValidationError(
189 _('Please provide a more realistic latitude '
190 'and longitude.')))
191 raise self._error
192 return LocationValue(latitude, longitude, time_zone)
1930
=== modified file 'lib/lp/app/widgets/templates/location.pt'
--- lib/lp/app/widgets/templates/location.pt 2010-09-21 03:30:43 +0000
+++ lib/lp/app/widgets/templates/location.pt 2012-02-17 12:13:23 +0000
@@ -2,8 +2,6 @@
2 xmlns:tal="http://xml.zope.org/namespaces/tal"2 xmlns:tal="http://xml.zope.org/namespaces/tal"
3 xmlns:i18n="http://xml.zope.org/namespaces/i18n"3 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
4 omit-tag="">4 omit-tag="">
5 <tal:latitude replace="structure view/latitude_widget/hidden" />
6 <tal:longitude replace="structure view/longitude_widget/hidden" />
7 <div>5 <div>
8 <tal:time-zone replace="structure view/time_zone_widget" />6 <tal:time-zone replace="structure view/time_zone_widget" />
9 </div>7 </div>
108
=== modified file 'lib/lp/blueprints/browser/tests/sprintattendance-views.txt'
--- lib/lp/blueprints/browser/tests/sprintattendance-views.txt 2010-07-30 12:56:27 +0000
+++ lib/lp/blueprints/browser/tests/sprintattendance-views.txt 2012-02-17 12:13:23 +0000
@@ -204,23 +204,3 @@
204204
205 >>> print lines[-1]205 >>> print lines[-1]
206 name12,Sample Person,...Australia/Perth,...True206 name12,Sample Person,...Australia/Perth,...True
207
208However, some people may set their location/timezone as hidden, so if
209that's the case we won't include the person's timezone.
210
211 >>> person = factory.makePerson(
212 ... name='ubz-last-attendee', time_zone='Europe/London')
213 >>> login_person(person)
214 >>> person.setLocationVisibility(False)
215 >>> dates = ['2005-10-17 09:00', '2005-11-17 19:05']
216 >>> create_sprint_attendance_view(ubz, dates).initialize()
217
218 >>> view = create_view(ubz, '+attendees-csv')
219 >>> last_attendee = view.render().split('\n')[-2]
220 >>> print last_attendee
221 ubz-last-attendee,...
222
223 >>> 'Europe' in last_attendee
224 False
225
226
227207
=== modified file 'lib/lp/registry/browser/configure.zcml'
--- lib/lp/registry/browser/configure.zcml 2012-02-13 21:48:25 +0000
+++ lib/lp/registry/browser/configure.zcml 2012-02-17 12:13:23 +0000
@@ -841,7 +841,7 @@
841 <browser:page841 <browser:page
842 name="+editlocation"842 name="+editlocation"
843 for="lp.registry.interfaces.person.IPerson"843 for="lp.registry.interfaces.person.IPerson"
844 class="lp.registry.browser.person.PersonEditLocationView"844 class="lp.registry.browser.person.PersonEditTimeZoneView"
845 permission="launchpad.Edit"845 permission="launchpad.Edit"
846 template="../../app/templates/generic-edit.pt"/>846 template="../../app/templates/generic-edit.pt"/>
847 <browser:page847 <browser:page
@@ -946,9 +946,6 @@
946 <browser:page946 <browser:page
947 name="+xrds"947 name="+xrds"
948 attribute="xrds"/>948 attribute="xrds"/>
949 <browser:page
950 name="+portlet-map"
951 template="../templates/person-portlet-map.pt"/>
952 </browser:pages>949 </browser:pages>
953 <browser:page950 <browser:page
954 for="lp.registry.interfaces.person.IPerson"951 for="lp.registry.interfaces.person.IPerson"
@@ -1070,12 +1067,6 @@
1070 template="../templates/team-portlet-polls.pt"/>1067 template="../templates/team-portlet-polls.pt"/>
1071 <browser:page1068 <browser:page
1072 for="lp.registry.interfaces.person.ITeam"1069 for="lp.registry.interfaces.person.ITeam"
1073 permission="zope.Public"
1074 class="lp.registry.browser.team.TeamMapView"
1075 name="+map"
1076 template="../templates/team-map.pt"/>
1077 <browser:page
1078 for="lp.registry.interfaces.person.ITeam"
1079 class="lp.registry.browser.team.TeamMailingListSubscribersView"1070 class="lp.registry.browser.team.TeamMailingListSubscribersView"
1080 permission="zope.Public"1071 permission="zope.Public"
1081 name="+mailing-list-subscribers"1072 name="+mailing-list-subscribers"
@@ -1083,18 +1074,6 @@
1083 <browser:page1074 <browser:page
1084 for="lp.registry.interfaces.person.ITeam"1075 for="lp.registry.interfaces.person.ITeam"
1085 permission="zope.Public"1076 permission="zope.Public"
1086 class="lp.registry.browser.team.TeamMapData"
1087 template="../templates/team-map-data.pt"
1088 name="+mapdata"/>
1089 <browser:page
1090 for="lp.registry.interfaces.person.ITeam"
1091 permission="zope.Public"
1092 class="lp.registry.browser.team.TeamMapLtdData"
1093 template="../templates/team-map-data.pt"
1094 name="+mapdataltd"/>
1095 <browser:page
1096 for="lp.registry.interfaces.person.ITeam"
1097 permission="zope.Public"
1098 name="+listing-simple"1077 name="+listing-simple"
1099 template="../templates/team-listing-simple.pt"/>1078 template="../templates/team-listing-simple.pt"/>
1100 <browser:page1079 <browser:page
@@ -1105,12 +1084,6 @@
1105 class="lp.registry.browser.team.TeamMugshotView"/>1084 class="lp.registry.browser.team.TeamMugshotView"/>
1106 <browser:page1085 <browser:page
1107 for="lp.registry.interfaces.person.ITeam"1086 for="lp.registry.interfaces.person.ITeam"
1108 class="lp.registry.browser.team.TeamMapLtdView"
1109 permission="zope.Public"
1110 name="+portlet-map"
1111 template="../templates/team-portlet-map.pt"/>
1112 <browser:page
1113 for="lp.registry.interfaces.person.ITeam"
1114 class="lp.registry.browser.team.TeamIndexView"1087 class="lp.registry.browser.team.TeamIndexView"
1115 permission="zope.Public"1088 permission="zope.Public"
1116 name="+portlet-membership"1089 name="+portlet-membership"
11171090
=== modified file 'lib/lp/registry/browser/person.py'
--- lib/lp/registry/browser/person.py 2012-02-14 15:32:31 +0000
+++ lib/lp/registry/browser/person.py 2012-02-17 12:13:23 +0000
@@ -26,7 +26,7 @@
26 'PersonEditHomePageView',26 'PersonEditHomePageView',
27 'PersonEditIRCNicknamesView',27 'PersonEditIRCNicknamesView',
28 'PersonEditJabberIDsView',28 'PersonEditJabberIDsView',
29 'PersonEditLocationView',29 'PersonEditTimeZoneView',
30 'PersonEditSSHKeysView',30 'PersonEditSSHKeysView',
31 'PersonEditView',31 'PersonEditView',
32 'PersonFacets',32 'PersonFacets',
@@ -114,7 +114,6 @@
114from zope.interface.interface import invariant114from zope.interface.interface import invariant
115from zope.publisher.interfaces import NotFound115from zope.publisher.interfaces import NotFound
116from zope.schema import (116from zope.schema import (
117 Bool,
118 Choice,117 Choice,
119 Text,118 Text,
120 TextLine,119 TextLine,
@@ -155,7 +154,6 @@
155 LaunchpadRadioWidget,154 LaunchpadRadioWidget,
156 LaunchpadRadioWidgetWithDescription,155 LaunchpadRadioWidgetWithDescription,
157 )156 )
158from lp.app.widgets.location import LocationWidget
159from lp.blueprints.browser.specificationtarget import HasSpecificationsView157from lp.blueprints.browser.specificationtarget import HasSpecificationsView
160from lp.blueprints.enums import SpecificationFilter158from lp.blueprints.enums import SpecificationFilter
161from lp.bugs.browser.bugtask import BugTaskSearchListingView159from lp.bugs.browser.bugtask import BugTaskSearchListingView
@@ -222,7 +220,6 @@
222from lp.services.config import config220from lp.services.config import config
223from lp.services.database.sqlbase import flush_database_updates221from lp.services.database.sqlbase import flush_database_updates
224from lp.services.feeds.browser import FeedsMixin222from lp.services.feeds.browser import FeedsMixin
225from lp.services.fields import LocationField
226from lp.services.geoip.interfaces import IRequestPreferredLanguages223from lp.services.geoip.interfaces import IRequestPreferredLanguages
227from lp.services.gpg.interfaces import (224from lp.services.gpg.interfaces import (
228 GPGKeyNotFoundError,225 GPGKeyNotFoundError,
@@ -3024,47 +3021,6 @@
3024 "mailing list."))3021 "mailing list."))
3025 self.request.response.redirect(canonical_url(self.context))3022 self.request.response.redirect(canonical_url(self.context))
30263023
3027 @property
3028 def map_portlet_html(self):
3029 """Generate the HTML which shows the map portlet."""
3030 assert self.has_visible_location, (
3031 "Can't generate the map for a person who hasn't set a "
3032 "visible location.")
3033 replacements = {'center_lat': self.context.latitude,
3034 'center_lng': self.context.longitude}
3035 return u"""
3036 <script type="text/javascript">
3037 LPJS.use('node', 'lp.app.mapping', function(Y) {
3038 function renderMap() {
3039 Y.lp.app.mapping.renderPersonMapSmall(
3040 %(center_lat)s, %(center_lng)s);
3041 }
3042 Y.on("domready", renderMap);
3043 });
3044 </script>""" % replacements
3045
3046 @cachedproperty
3047 def has_visible_location(self):
3048 """Does the person have latitude and a visible location."""
3049 if self.context.is_team:
3050 return self.context.mapped_participants_count > 0
3051 else:
3052 return (check_permission('launchpad.View', self.context.location)
3053 and self.context.latitude is not None)
3054
3055 @property
3056 def should_show_map_portlet(self):
3057 """Should the map portlet be displayed?
3058
3059 The map portlet is displayed only if the person has no location
3060 specified (latitude), or if the user has permission to view the
3061 person's location.
3062 """
3063 if self.user == self.context:
3064 return True
3065 else:
3066 return self.has_visible_location
3067
30683024
3069class PersonCodeOfConductEditView(LaunchpadView):3025class PersonCodeOfConductEditView(LaunchpadView):
3070 """View for the ~person/+codesofconduct pages."""3026 """View for the ~person/+codesofconduct pages."""
@@ -4762,22 +4718,19 @@
4762 canonical_url(self.context, view_name='+oauth-tokens'))4718 canonical_url(self.context, view_name='+oauth-tokens'))
47634719
47644720
4765class PersonLocationForm(Interface):4721class PersonTimeZoneForm(Interface):
47664722
4767 location = LocationField(4723 time_zone = Choice(
4768 title=_('Time zone'),4724 vocabulary='TimezoneName', title=_('Time zone'), required=True,
4769 required=True)4725 description=_(
4770 hide = Bool(4726 'Once the time zone is correctly set, events '
4771 title=_("Hide my location details from others."),4727 'in Launchpad will be displayed in local time.'))
4772 required=True, default=False)4728
47734729
47744730class PersonEditTimeZoneView(LaunchpadFormView):
4775class PersonEditLocationView(LaunchpadFormView):4731 """Edit a person's time zone."""
4776 """Edit a person's location."""4732
47774733 schema = PersonTimeZoneForm
4778 schema = PersonLocationForm
4779 field_names = ['location']
4780 custom_widget('location', LocationWidget)
4781 page_title = label = 'Set timezone'4734 page_title = label = 'Set timezone'
47824735
4783 @property4736 @property
@@ -4788,17 +4741,14 @@
47884741
4789 @action(_("Update"), name="update")4742 @action(_("Update"), name="update")
4790 def action_update(self, action, data):4743 def action_update(self, action, data):
4791 """Set the coordinates and time zone for the person."""4744 """Set the time zone for the person."""
4792 new_location = data.get('location')4745 timezone = data.get('time_zone')
4793 if new_location is None:4746 if timezone is None:
4794 raise UnexpectedFormData('No location received.')4747 raise UnexpectedFormData('No location received.')
4795 latitude = new_location.latitude4748 # XXX salgado, 2012-02-16, bug=933699: Use setLocation() because it's
4796 longitude = new_location.longitude4749 # the cheaper way to set the timezone of a person. Once the bug is
4797 time_zone = new_location.time_zone4750 # fixed we'll be able to get rid of this hack.
4798 self.context.setLocation(latitude, longitude, time_zone, self.user)4751 self.context.setLocation(None, None, timezone, self.user)
4799 if 'hide' in self.field_names:
4800 visible = not data['hide']
4801 self.context.setLocationVisibility(visible)
48024752
48034753
4804def archive_to_person(archive):4754def archive_to_person(archive):
48054755
=== modified file 'lib/lp/registry/browser/team.py'
--- lib/lp/registry/browser/team.py 2012-02-16 03:12:17 +0000
+++ lib/lp/registry/browser/team.py 2012-02-17 12:13:23 +0000
@@ -20,10 +20,6 @@
20 'TeamMailingListModerationView',20 'TeamMailingListModerationView',
21 'TeamMailingListSubscribersView',21 'TeamMailingListSubscribersView',
22 'TeamMailingListArchiveView',22 'TeamMailingListArchiveView',
23 'TeamMapData',
24 'TeamMapLtdData',
25 'TeamMapView',
26 'TeamMapLtdView',
27 'TeamMemberAddView',23 'TeamMemberAddView',
28 'TeamMembershipView',24 'TeamMembershipView',
29 'TeamMugshotView',25 'TeamMugshotView',
@@ -1206,112 +1202,6 @@
1206 self.widgets['newmember'].setRenderedValue(None)1202 self.widgets['newmember'].setRenderedValue(None)
12071203
12081204
1209class TeamMapView(LaunchpadView):
1210 """Show all people with known locations on a map.
1211
1212 Also provides links to edit the locations of people in the team without
1213 known locations.
1214 """
1215
1216 label = "Team member locations"
1217 limit = None
1218
1219 @cachedproperty
1220 def mapped_participants(self):
1221 """Participants with locations."""
1222 return self.context.getMappedParticipants(limit=self.limit)
1223
1224 @cachedproperty
1225 def mapped_participants_count(self):
1226 """Count of participants with locations."""
1227 return self.context.mapped_participants_count
1228
1229 @cachedproperty
1230 def has_mapped_participants(self):
1231 """Does the team have any mapped participants?"""
1232 return self.mapped_participants_count > 0
1233
1234 @cachedproperty
1235 def unmapped_participants(self):
1236 """Participants (ordered by name) with no recorded locations."""
1237 return list(self.context.unmapped_participants)
1238
1239 @cachedproperty
1240 def unmapped_participants_count(self):
1241 """Count of participants with no recorded locations."""
1242 return self.context.unmapped_participants_count
1243
1244 @cachedproperty
1245 def times(self):
1246 """The current times in time zones with members."""
1247 zones = set(participant.time_zone
1248 for participant in self.mapped_participants)
1249 times = [datetime.now(pytz.timezone(zone))
1250 for zone in zones]
1251 timeformat = '%H:%M'
1252 return sorted(
1253 set(time.strftime(timeformat) for time in times))
1254
1255 @cachedproperty
1256 def bounds(self):
1257 """A dictionary with the bounds and center of the map, or None"""
1258 if self.has_mapped_participants:
1259 return self.context.getMappedParticipantsBounds(self.limit)
1260 return None
1261
1262 @property
1263 def map_html(self):
1264 """HTML which shows the map with location of the team's members."""
1265 return """
1266 <script type="text/javascript">
1267 LPJS.use('node', 'lp.app.mapping', function(Y) {
1268 function renderMap() {
1269 Y.lp.app.mapping.renderTeamMap(
1270 %(min_lat)s, %(max_lat)s, %(min_lng)s,
1271 %(max_lng)s, %(center_lat)s, %(center_lng)s);
1272 }
1273 Y.on("domready", renderMap);
1274 });
1275 </script>""" % self.bounds
1276
1277 @property
1278 def map_portlet_html(self):
1279 """The HTML which shows a small version of the team's map."""
1280 return """
1281 <script type="text/javascript">
1282 LPJS.use('node', 'lp.app.mapping', function(Y) {
1283 function renderMap() {
1284 Y.lp.app.mapping.renderTeamMapSmall(
1285 %(center_lat)s, %(center_lng)s);
1286 }
1287 Y.on("domready", renderMap);
1288 });
1289 </script>""" % self.bounds
1290
1291
1292class TeamMapData(TeamMapView):
1293 """An XML dump of the locations of all team members."""
1294
1295 def render(self):
1296 self.request.response.setHeader(
1297 'content-type', 'application/xml;charset=utf-8')
1298 body = LaunchpadView.render(self)
1299 return body.encode('utf-8')
1300
1301
1302class TeamMapLtdMixin:
1303 """A mixin for team views with limited participants."""
1304 limit = 24
1305
1306
1307class TeamMapLtdView(TeamMapLtdMixin, TeamMapView):
1308 """Team map view with limited participants."""
1309
1310
1311class TeamMapLtdData(TeamMapLtdMixin, TeamMapData):
1312 """An XML dump of the locations of limited number of team members."""
1313
1314
1315class TeamNavigation(PersonNavigation):1205class TeamNavigation(PersonNavigation):
13161206
1317 usedfor = ITeam1207 usedfor = ITeam
@@ -1603,11 +1493,6 @@
1603 text = 'Approve or decline members'1493 text = 'Approve or decline members'
1604 return Link(target, text, icon='add')1494 return Link(target, text, icon='add')
16051495
1606 def map(self):
1607 target = '+map'
1608 text = 'View map and time zones'
1609 return Link(target, text, icon='meeting')
1610
1611 def add_my_teams(self):1496 def add_my_teams(self):
1612 target = '+add-my-teams'1497 target = '+add-my-teams'
1613 text = 'Add one of my teams'1498 text = 'Add one of my teams'
@@ -1723,7 +1608,6 @@
1723 'configure_mailing_list',1608 'configure_mailing_list',
1724 'moderate_mailing_list',1609 'moderate_mailing_list',
1725 'editlanguages',1610 'editlanguages',
1726 'map',
1727 'polls',1611 'polls',
1728 'add_poll',1612 'add_poll',
1729 'join',1613 'join',
17301614
=== modified file 'lib/lp/registry/browser/tests/team-views.txt'
--- lib/lp/registry/browser/tests/team-views.txt 2012-02-10 19:38:27 +0000
+++ lib/lp/registry/browser/tests/team-views.txt 2012-02-17 12:13:23 +0000
@@ -67,135 +67,6 @@
67 form fields.67 form fields.
6868
6969
70+map-portlet
71------------
72
73The team profile page contain the location portlet that shows a map.
74
75 >>> team_view = create_initialized_view(ubuntu_team, '+index')
76 >>> team_view.has_visible_location
77 False
78
79After a member has set his location, the map will be rendered.
80
81 >>> login_person(sample_person)
82 >>> sample_person.setLocation(
83 ... 38.81, 77.1, 'America/New_York', sample_person)
84
85 >>> team_view = create_initialized_view(ubuntu_team, '+index')
86 >>> team_view.has_visible_location
87 True
88
89The small_maps key in the launchpad_views cookie can be set by the viewing
90user to 'false' to indicate that small maps are not wanted.
91
92 >>> cookie = 'launchpad_views=small_maps=false'
93 >>> team_view = create_initialized_view(
94 ... ubuntu_team, '+index', cookie=cookie)
95
96+map
97----
98
99A team's +map page will show a map with markers on the location of all
100members of that team. That page also lists the local times of members.
101
102 >>> login('foo.bar@canonical.com')
103 >>> context = factory.makeTeam(salgado)
104 >>> context.mapped_participants_count
105 0
106 >>> view = create_initialized_view(context, '+map')
107 >>> view.times
108 []
109
110
111Once a member is mapped, the map will be rendered. The view provides a cached
112property to access the mapped participants. The views number of times is
113incremented for each timezone the members reside in.
114
115 >>> london_member = factory.makePerson(
116 ... latitude=51.49, longitude=-0.13, time_zone='Europe/London')
117 >>> ignored = context.addMember(london_member, mark)
118 >>> context.mapped_participants_count
119 1
120 >>> view = create_initialized_view(context, '+map')
121 >>> len(view.mapped_participants)
122 1
123 >>> len(view.times)
124 1
125
126 >>> brazil_member = factory.makePerson(
127 ... latitude=-23.60, longitude=-46.64, time_zone='America/Sao_Paulo')
128 >>> ignored = context.addMember(brazil_member, mark)
129 >>> context.mapped_participants_count
130 2
131 >>> view = create_initialized_view(context, '+map')
132 >>> len(view.mapped_participants)
133 2
134 >>> len(view.times)
135 2
136
137If we had two members in London, though, we wouldn't list London's time
138twice, for obvious reasons.
139
140 >>> london_member2 = factory.makePerson(
141 ... latitude=51.49, longitude=-0.13, time_zone='Europe/London')
142 >>> ignored = context.addMember(london_member2, mark)
143 >>> context.mapped_participants_count
144 3
145 >>> view = create_initialized_view(context, '+map')
146 >>> len(view.times)
147 2
148
149The number of persons returned by mapped_participants is governed by the
150view's limit attribute. The default value is None, meaning there is no
151limit. This view is used for displaying the full map.
152
153 >>> print view.limit
154 None
155 >>> len(view.mapped_participants)
156 3
157
158A portlet for the map also exists which is used for displaying the map
159inside the person or team page. It has the number of participants limited.
160
161 >>> view = create_initialized_view(context, '+portlet-map')
162 >>> view.limit
163 24
164
165 # Hack the limit to demonstrate that it controls mapped_participants.
166 >>> view.limit = 1
167 >>> len(view.mapped_participants)
168 1
169
170The view provides the count of mapped and unmapped members. The counts
171are cached to improve performance.
172
173 >>> view = create_initialized_view(context, '+map')
174 >>> view.mapped_participants_count
175 3
176 >>> view.unmapped_participants_count
177 1
178
179The template can ask if the team has mapped members, so that it does not
180need to work with counts or the list of members.
181
182 >>> view.has_mapped_participants
183 True
184
185The cached bounds of the mapped members of the team can be used to define
186the scale of a map.
187
188 >>> bounds = view.bounds
189 >>> for key in sorted(bounds):
190 ... print "%s: %s" % (key, bounds[key])
191 center_lat: 13...
192 center_lng: -23...
193 max_lat: 51...
194 max_lng: -0...
195 min_lat: -23...
196 min_lng: -46...
197
198
199Contacting the team70Contacting the team
200-------------------71-------------------
20172
20273
=== modified file 'lib/lp/registry/doc/personlocation.txt'
--- lib/lp/registry/doc/personlocation.txt 2011-12-24 17:49:30 +0000
+++ lib/lp/registry/doc/personlocation.txt 2012-02-17 12:13:23 +0000
@@ -1,219 +1,41 @@
1Locations for People and Teams1Locations for People and Teams
2==============================2==============================
33
4The PersonLocation object stores information about the location and time4We no longer allow people to set their geographical locations, but their time
5zone of a person. It also remembers who provided that information, and5zone (which they can set) is stored together with their latitude/longitude (in
6when. This is designed to make it possible to have people provide6PersonLocation). Until we move the time zone back to the Person table
7location / time zone info for other people in a wiki style.7(bug=933699), we'll maintain the setLocation() API on IPerson.
88
9 >>> from lp.services.webapp.testing import verifyObject9 >>> from lp.services.webapp.testing import verifyObject
10 >>> from lp.registry.interfaces.location import IObjectWithLocation
11 >>> from lp.registry.interfaces.person import IPersonSet10 >>> from lp.registry.interfaces.person import IPersonSet
12 >>> personset = getUtility(IPersonSet)11 >>> personset = getUtility(IPersonSet)
1312
14A Person implements the IObjectWithLocation interface.
15
16 >>> login('test@canonical.com')
17 >>> marilize = personset.getByName('marilize')13 >>> marilize = personset.getByName('marilize')
18 >>> verifyObject(IObjectWithLocation, marilize)
19 True
20
21A Person has a PersonLocation record, if there is any location
22information associated with them. That implements the IPersonLocation
23interface.
24
25 >>> from lp.registry.interfaces.location import IPersonLocation
26 >>> marilize.location
27 <PersonLocation...
28 >>> verifyObject(IPersonLocation, marilize.location)
29 True
30
31In some cases, a person has a time zone, but no location.
32
33 >>> print marilize.time_zone14 >>> print marilize.time_zone
34 Africa/Maseru15 Africa/Maseru
35 >>> print marilize.latitude16 >>> print marilize.latitude
36 None17 None
3718 >>> print marilize.longitude
38The location for a person is set with the "setLocation" method. This19 None
39requires that the user providing the information is passed as a20
40parameter.21setLocation() will always set the time zone to the given value and both
4122latitude and longitude to None, regardless of what was passed in.
42A user cannot set another user's location.23
43
44 >>> jdub = personset.getByName('jdub')
45 >>> login_person(jdub)
46 >>> cprov = personset.getByName('cprov')24 >>> cprov = personset.getByName('cprov')
47 >>> cprov.setLocation(-43.0, -62.1, 'America/Sao_Paulo', jdub)
48 Traceback (most recent call last):
49 ...
50 Unauthorized:...
51
52A user can set his own location.
53
54 >>> login_person(cprov)25 >>> login_person(cprov)
55 >>> cprov.setLocation(-43.2, -61.93, 'America/Sao_Paulo', cprov)26 >>> cprov.setLocation(-43.2, -61.93, 'America/Sao_Paulo', cprov)
5627 >>> print cprov.time_zone
57cprov can change his location information. We need to deal28 America/Sao_Paulo
58with some floating point precision issues here, hence the rounding.29 >>> print cprov.latitude
5930 None
60 >>> login_person(cprov)31 >>> print cprov.longitude
61 >>> cprov.setLocation(-43.52, -61.93, 'America/Sao_Paulo', cprov)32 None
62 >>> abs(cprov.latitude + 43.52) < 0.001
63 True
64
65Admins can set a user's location.
66
67 >>> admin = personset.getByName('name16')
68 >>> login_person(admin)
69 >>> cprov.setLocation(-43.0, -62.1, 'America/Sao_Paulo', admin)
70 >>> abs(cprov.longitude + 62.1) < 0.001
71 True
7233
73We cannot store a location for a team, though.34We cannot store a location for a team, though.
7435
36 >>> jdub = personset.getByName('jdub')
75 >>> guadamen = personset.getByName('guadamen')37 >>> guadamen = personset.getByName('guadamen')
76 >>> guadamen.setLocation(34.5, 23.1, 'Africa/Maseru', jdub)38 >>> guadamen.setLocation(34.5, 23.1, 'Africa/Maseru', jdub)
77 Traceback (most recent call last):39 Traceback (most recent call last):
78 ...40 ...
79 AssertionError:...41 AssertionError:...
80
81Nor can we set only the latitude of a person.
82
83 >>> cprov.setLocation(-43.0, None, 'America/Sao_Paulo', admin)
84 Traceback (most recent call last):
85 ...
86 AssertionError:...
87
88Similarly, we can't set only the longitude.
89
90 >>> cprov.setLocation(None, -43.0, 'America/Sao_Paulo', admin)
91 Traceback (most recent call last):
92 ...
93 AssertionError:...
94
95We can get lists of the participants in a team that do, or do not, have
96locations. Specifically, we mean latitude/longitude data, not time zone
97data.
98
99When we get mapped participants, and unmapped participants, we only mean
100the individuals, not other teams. We'll show that guadamen has a
101sub-team, ubuntu-team, and that it still does not appear in either
102mapped_participants or unmapped_participants (although its members do).
103
104 >>> for member in guadamen.activemembers:
105 ... if member.teamowner is not None:
106 ... print member.name
107 ubuntu-team
108 >>> len(guadamen.getMappedParticipants())
109 2
110 >>> for mapped in guadamen.getMappedParticipants():
111 ... if mapped.teamowner is not None:
112 ... print mapped.name
113 >>> guadamen.unmapped_participants.count()
114 7
115 >>> for unmapped in guadamen.unmapped_participants:
116 ... if unmapped.teamowner is not None:
117 ... print unmapped.name
118
119When we iterate over the mapped_participants in a team, their locations
120have been pre-cached so that we don't hit the database everytime we
121access a person's .location property.
122
123 >>> from lp.services.propertycache import get_property_cache
124 >>> for mapped in guadamen.getMappedParticipants():
125 ... cache = get_property_cache(mapped)
126 ... if ("location" not in cache or
127 ... not verifyObject(IPersonLocation, cache.location)):
128 ... print 'No cached location on %s' % mapped.name
129
130The mapped_participants methods takes a optional argument to limit the
131number of persons in the returned list.
132
133 >>> mapped = guadamen.getMappedParticipants(limit=1)
134 >>> len(mapped)
135 1
136
137The count of mapped and unmapped members can also be retrieved, which is
138faster than getting the resultset of members.
139
140 >>> guadamen.mapped_participants_count
141 2
142 >>> guadamen.unmapped_participants_count
143 7
144
145The bounds of the mapped members can be retrieved. It is a dict that contains
146the minimum maximum, and central longitudes and latitudes.
147
148 >>> bounds = guadamen.getMappedParticipantsBounds()
149 >>> for key in sorted(bounds):
150 ... print "%s: %s" % (key, bounds[key])
151 center_lat: 4...
152 center_lng: -30...
153 max_lat: 52...
154 max_lng: 0...
155 min_lat: -43...
156 min_lng: -62...
157
158Calling getMappedParticipantsBounds() on a team without members is an error.
159
160 >>> unmapped_team = factory.makeTeam()
161 >>> unmapped_team.getMappedParticipantsBounds()
162 Traceback (most recent call last):
163 ...
164 AssertionError: This method cannot be called when
165 mapped_participants_count == 0.
166
167
168Location visibility
169-------------------
170
171Some people may not want their location to be disclosed to others, so
172we provide a way for them to hide their location from other users. By
173default a person's location is visible.
174
175 >>> salgado = personset.getByName('salgado')
176 >>> login_person(salgado)
177 >>> salgado.setLocation(-43.0, -62.1, 'America/Sao_Paulo', salgado)
178 >>> salgado.location.visible
179 True
180 >>> salgado.location.latitude
181 -43...
182
183 >>> login_person(jdub)
184 >>> salgado.location.latitude
185 -43...
186
187But it can be changed through the setLocationVisibility() method. If the
188visibility is set to False, only the person himself will be able to see
189the location data except for time zone.
190
191 >>> login_person(salgado)
192 >>> salgado.setLocationVisibility(False)
193 >>> salgado.location.visible
194 False
195
196 >>> login_person(jdub)
197 >>> print salgado.time_zone
198 America/Sao_Paulo
199 >>> salgado.latitude
200 Traceback (most recent call last):
201 ...
202 Unauthorized:...
203 >>> salgado.longitude
204 Traceback (most recent call last):
205 ...
206 Unauthorized:...
207 >>> salgado.location.latitude
208 Traceback (most recent call last):
209 ...
210 Unauthorized:...
211
212A team's .mapped_participants will also exclude the members who made
213their location invisible.
214
215 >>> admins = personset.getByName('admins')
216 >>> salgado in admins.activemembers
217 True
218 >>> salgado in admins.getMappedParticipants()
219 False
22042
=== modified file 'lib/lp/registry/interfaces/location.py'
--- lib/lp/registry/interfaces/location.py 2011-12-24 16:54:44 +0000
+++ lib/lp/registry/interfaces/location.py 2012-02-17 12:13:23 +0000
@@ -47,12 +47,18 @@
47class IHasLocation(Interface):47class IHasLocation(Interface):
48 """An interface supported by objects with a defined location."""48 """An interface supported by objects with a defined location."""
4949
50 latitude = exported(doNotSnapshot(50 latitude = exported(
51 Float(title=_("The latitude of this object."),51 doNotSnapshot(
52 required=False, readonly=True)))52 Float(title=_("The latitude of this object."),
53 longitude = exported(doNotSnapshot(53 required=False, readonly=True)),
54 Float(title=_("The longitude of this object."),54 ('devel', dict(exported=False)),
55 required=False, readonly=True)))55 exported=True)
56 longitude = exported(
57 doNotSnapshot(
58 Float(title=_("The longitude of this object."),
59 required=False, readonly=True)),
60 ('devel', dict(exported=False)),
61 exported=True)
56 time_zone = exported(doNotSnapshot(62 time_zone = exported(doNotSnapshot(
57 Choice(title=_('The time zone of this object.'),63 Choice(title=_('The time zone of this object.'),
58 required=False, readonly=True,64 required=False, readonly=True,
5965
=== modified file 'lib/lp/registry/interfaces/person.py'
--- lib/lp/registry/interfaces/person.py 2012-02-16 08:27:37 +0000
+++ lib/lp/registry/interfaces/person.py 2012-02-17 12:13:23 +0000
@@ -130,7 +130,6 @@
130from lp.registry.interfaces.jabber import IJabberID130from lp.registry.interfaces.jabber import IJabberID
131from lp.registry.interfaces.location import (131from lp.registry.interfaces.location import (
132 IHasLocation,132 IHasLocation,
133 ILocationRecord,
134 IObjectWithLocation,133 IObjectWithLocation,
135 ISetLocation,134 ISetLocation,
136 )135 )
@@ -719,7 +718,7 @@
719 @operation_for_version("beta")718 @operation_for_version("beta")
720 def transitionVisibility(visibility, user):719 def transitionVisibility(visibility, user):
721 """Set visibility of IPerson.720 """Set visibility of IPerson.
722 721
723 :param visibility: The PersonVisibility to change to.722 :param visibility: The PersonVisibility to change to.
724 :param user: The user requesting the change.723 :param user: The user requesting the change.
725 :raises: `ImmutableVisibilityError` when the visibility can not724 :raises: `ImmutableVisibilityError` when the visibility can not
@@ -1611,31 +1610,6 @@
1611 exported_as='proposed_members')1610 exported_as='proposed_members')
1612 proposed_member_count = Attribute("Number of PROPOSED members")1611 proposed_member_count = Attribute("Number of PROPOSED members")
16131612
1614 mapped_participants_count = Attribute(
1615 "The number of mapped participants")
1616 unmapped_participants = doNotSnapshot(
1617 CollectionField(
1618 title=_("List of participants with no coordinates recorded."),
1619 value_type=Reference(schema=Interface)))
1620 unmapped_participants_count = Attribute(
1621 "The number of unmapped participants")
1622
1623 def getMappedParticipants(limit=None):
1624 """List of participants with coordinates.
1625
1626 :param limit: The optional maximum number of items to return.
1627 :return: A list of `IPerson` objects
1628 """
1629
1630 def getMappedParticipantsBounds():
1631 """Return a dict of the bounding longitudes latitudes, and centers.
1632
1633 This method cannot be called if there are no mapped participants.
1634
1635 :return: a dict containing: min_lat, min_lng, max_lat, max_lng,
1636 center_lat, and center_lng
1637 """
1638
1639 def getMembersWithPreferredEmails():1613 def getMembersWithPreferredEmails():
1640 """Returns a result set of persons with precached addresses.1614 """Returns a result set of persons with precached addresses.
16411615
@@ -1711,13 +1685,6 @@
1711 :param team: The team to leave.1685 :param team: The team to leave.
1712 """1686 """
17131687
1714 @operation_parameters(
1715 visible=copy_field(ILocationRecord['visible'], required=True))
1716 @export_write_operation()
1717 @operation_for_version("beta")
1718 def setLocationVisibility(visible):
1719 """Specify the visibility of a person's location and time zone."""
1720
1721 def setMembershipData(person, status, reviewer, expires=None,1688 def setMembershipData(person, status, reviewer, expires=None,
1722 comment=None):1689 comment=None):
1723 """Set the attributes of the person's membership on this team.1690 """Set the attributes of the person's membership on this team.
@@ -2585,7 +2552,6 @@
2585 'invited_members',2552 'invited_members',
2586 'deactivatedmembers',2553 'deactivatedmembers',
2587 'expiredmembers',2554 'expiredmembers',
2588 'unmapped_participants',
2589 ]:2555 ]:
2590 IPersonViewRestricted[name].value_type.schema = IPerson2556 IPersonViewRestricted[name].value_type.schema = IPerson
25912557
25922558
=== modified file 'lib/lp/registry/model/person.py'
--- lib/lp/registry/model/person.py 2012-02-16 08:27:37 +0000
+++ lib/lp/registry/model/person.py 2012-02-17 12:13:23 +0000
@@ -705,6 +705,24 @@
705 Or(OAuthRequestToken.date_expires == None,705 Or(OAuthRequestToken.date_expires == None,
706 OAuthRequestToken.date_expires > UTC_NOW))706 OAuthRequestToken.date_expires > UTC_NOW))
707707
708 @property
709 def latitude(self):
710 """See `IHasLocation`.
711
712 We no longer allow users to set their geographical location but we
713 need to keep this because it was exported on version 1.0 of the API.
714 """
715 return None
716
717 @property
718 def longitude(self):
719 """See `IHasLocation`.
720
721 We no longer allow users to set their geographical location but we
722 need to keep this because it was exported on version 1.0 of the API.
723 """
724 return None
725
708 @cachedproperty726 @cachedproperty
709 def location(self):727 def location(self):
710 """See `IObjectWithLocation`."""728 """See `IObjectWithLocation`."""
@@ -719,33 +737,6 @@
719 # enough rights to see it.737 # enough rights to see it.
720 return ProxyFactory(self.location).time_zone738 return ProxyFactory(self.location).time_zone
721739
722 @property
723 def latitude(self):
724 """See `IHasLocation`."""
725 if self.location is None:
726 return None
727 # Wrap the location with a security proxy to make sure the user has
728 # enough rights to see it.
729 return ProxyFactory(self.location).latitude
730
731 @property
732 def longitude(self):
733 """See `IHasLocation`."""
734 if self.location is None:
735 return None
736 # Wrap the location with a security proxy to make sure the user has
737 # enough rights to see it.
738 return ProxyFactory(self.location).longitude
739
740 def setLocationVisibility(self, visible):
741 """See `ISetLocation`."""
742 assert not self.is_team, 'Cannot edit team location.'
743 if self.location is None:
744 get_property_cache(self).location = PersonLocation(
745 person=self, visible=visible)
746 else:
747 self.location.visible = visible
748
749 def setLocation(self, latitude, longitude, time_zone, user):740 def setLocation(self, latitude, longitude, time_zone, user):
750 """See `ISetLocation`."""741 """See `ISetLocation`."""
751 assert not self.is_team, 'Cannot edit team location.'742 assert not self.is_team, 'Cannot edit team location.'
@@ -2062,92 +2053,6 @@
2062 )2053 )
2063 return (self.subscriptionpolicy in open_types)2054 return (self.subscriptionpolicy in open_types)
20642055
2065 def _getMappedParticipantsLocations(self, limit=None):
2066 """See `IPersonViewRestricted`."""
2067 return PersonLocation.select("""
2068 PersonLocation.person = TeamParticipation.person AND
2069 TeamParticipation.team = %s AND
2070 -- We only need to check for a latitude here because there's a DB
2071 -- constraint which ensures they are both set or unset.
2072 PersonLocation.latitude IS NOT NULL AND
2073 PersonLocation.visible IS TRUE AND
2074 Person.id = PersonLocation.person AND
2075 Person.teamowner IS NULL
2076 """ % sqlvalues(self.id),
2077 clauseTables=['TeamParticipation', 'Person'],
2078 prejoins=['person', ], limit=limit)
2079
2080 def getMappedParticipants(self, limit=None):
2081 """See `IPersonViewRestricted`."""
2082 # Pre-cache this location against its person. Since we'll always
2083 # iterate over all persons returned by this property (to build the map
2084 # of team members), it becomes more important to cache their locations
2085 # than to return a lazy SelectResults (or similar) object that only
2086 # fetches the rows when they're needed.
2087 locations = self._getMappedParticipantsLocations(limit=limit)
2088 for location in locations:
2089 get_property_cache(location.person).location = location
2090 participants = set(location.person for location in locations)
2091 # Cache the ValidPersonCache query for all mapped participants.
2092 if len(participants) > 0:
2093 sql = "id IN (%s)" % ",".join(sqlvalues(*participants))
2094 list(ValidPersonCache.select(sql))
2095 getUtility(IPersonSet).cacheBrandingForPeople(participants)
2096 return list(participants)
2097
2098 @property
2099 def mapped_participants_count(self):
2100 """See `IPersonViewRestricted`."""
2101 return self._getMappedParticipantsLocations().count()
2102
2103 def getMappedParticipantsBounds(self, limit=None):
2104 """See `IPersonViewRestricted`."""
2105 max_lat = -90.0
2106 min_lat = 90.0
2107 max_lng = -180.0
2108 min_lng = 180.0
2109 locations = self._getMappedParticipantsLocations(limit)
2110 if self.mapped_participants_count == 0:
2111 raise AssertionError(
2112 'This method cannot be called when '
2113 'mapped_participants_count == 0.')
2114 latitudes = sorted(location.latitude for location in locations)
2115 if latitudes[-1] > max_lat:
2116 max_lat = latitudes[-1]
2117 if latitudes[0] < min_lat:
2118 min_lat = latitudes[0]
2119 longitudes = sorted(location.longitude for location in locations)
2120 if longitudes[-1] > max_lng:
2121 max_lng = longitudes[-1]
2122 if longitudes[0] < min_lng:
2123 min_lng = longitudes[0]
2124 center_lat = (max_lat + min_lat) / 2.0
2125 center_lng = (max_lng + min_lng) / 2.0
2126 return dict(
2127 min_lat=min_lat, min_lng=min_lng, max_lat=max_lat,
2128 max_lng=max_lng, center_lat=center_lat, center_lng=center_lng)
2129
2130 @property
2131 def unmapped_participants(self):
2132 """See `IPersonViewRestricted`."""
2133 return Person.select("""
2134 Person.id = TeamParticipation.person AND
2135 TeamParticipation.team = %s AND
2136 TeamParticipation.person NOT IN (
2137 SELECT PersonLocation.person
2138 FROM PersonLocation INNER JOIN TeamParticipation ON
2139 PersonLocation.person = TeamParticipation.person
2140 WHERE TeamParticipation.team = %s AND
2141 PersonLocation.latitude IS NOT NULL) AND
2142 Person.teamowner IS NULL
2143 """ % sqlvalues(self.id, self.id),
2144 clauseTables=['TeamParticipation'])
2145
2146 @property
2147 def unmapped_participants_count(self):
2148 """See `IPersonViewRestricted`."""
2149 return self.unmapped_participants.count()
2150
2151 @property2056 @property
2152 def open_membership_invitations(self):2057 def open_membership_invitations(self):
2153 """See `IPerson`."""2058 """See `IPerson`."""
21542059
=== modified file 'lib/lp/registry/stories/location/personlocation-edit.txt'
--- lib/lp/registry/stories/location/personlocation-edit.txt 2012-01-15 13:32:27 +0000
+++ lib/lp/registry/stories/location/personlocation-edit.txt 2012-02-17 12:13:23 +0000
@@ -1,16 +1,15 @@
1Edit person location information1Edit person time zone information
2================================2=================================
33
4A person's location is only editable by people who have launchpad.Edit on4A person's time zone is only editable by people who have launchpad.Edit on
5the person, which is that person and admins.5the person, which is that person and admins.
66
7 >>> login('test@canonical.com')7 >>> login('test@canonical.com')
8 >>> zzz = factory.makePerson(8 >>> zzz = factory.makePerson(
9 ... name='zzz', time_zone='Africa/Maseru', email='zzz@foo.com',9 ... name='zzz', time_zone='Africa/Maseru', email='zzz@foo.com')
10 ... latitude=None, longitude=None)
11 >>> logout()10 >>> logout()
1211
13A user cannot set another user's location.12A user cannot set another user's +editlocation page.
1413
15 >>> nopriv_browser = setupBrowser(auth="Basic no-priv@canonical.com:test")14 >>> nopriv_browser = setupBrowser(auth="Basic no-priv@canonical.com:test")
16 >>> nopriv_browser.open('http://launchpad.dev/~zzz/+editlocation')15 >>> nopriv_browser.open('http://launchpad.dev/~zzz/+editlocation')
@@ -18,35 +17,15 @@
18 ...17 ...
19 Unauthorized:...18 Unauthorized:...
2019
21A user can set his own location:20A user can set his own time zone:
2221
23 >>> self_browser = setupBrowser(auth="Basic zzz@foo.com:test")22 >>> self_browser = setupBrowser(auth="Basic zzz@foo.com:test")
24 >>> self_browser.open('http://launchpad.dev/~zzz/+editlocation')23 >>> self_browser.open('http://launchpad.dev/~zzz/+editlocation')
25 >>> self_browser.getControl(name='field.location.latitude').value = (24 >>> self_browser.getControl(name='field.time_zone').value = [
26 ... '39.48')
27 >>> self_browser.getControl(name='field.location.longitude').value = (
28 ... '-0.40')
29 >>> self_browser.getControl(name='field.location.time_zone').value = [
30 ... 'Europe/Madrid']25 ... 'Europe/Madrid']
31 >>> self_browser.getControl('Update').click()26 >>> self_browser.getControl('Update').click()
3227
33 >>> login('zzz@foo.com')28 >>> login('zzz@foo.com')
34 >>> zzz.latitude
35 39.4...
36 >>> zzz.longitude
37 -0.4...
38 >>> zzz.time_zone29 >>> zzz.time_zone
39 u'Europe/Madrid'30 u'Europe/Madrid'
40 >>> logout()31 >>> logout()
41
42The user can change his location:
43
44 >>> self_browser.open('http://launchpad.dev/~zzz/+editlocation')
45 >>> self_browser.getControl(name='field.location.latitude').value
46 '39.48'
47
48And so can a Launchpad admin:
49
50 >>> admin_browser.open('http://launchpad.dev/~zzz/+editlocation')
51 >>> admin_browser.getControl(name='field.location.latitude').value
52 '39.48'
5332
=== removed file 'lib/lp/registry/stories/location/personlocation.txt'
--- lib/lp/registry/stories/location/personlocation.txt 2010-09-21 03:30:43 +0000
+++ lib/lp/registry/stories/location/personlocation.txt 1970-01-01 00:00:00 +0000
@@ -1,18 +0,0 @@
1Person Locations
2================
3
4People can have a location and time zone in Launchpad. In some cases, a
5person has a time zone, but no location. We test that their home page renders
6without problems.
7
8 >>> login('test@canonical.com')
9 >>> zzz = factory.makePerson(name='zzz', time_zone='Africa/Maseru',
10 ... latitude=None, longitude=None)
11 >>> logout()
12
13Any person can see another person's time zone if their is also
14location data set, otherwise the location portlet does not show up.
15
16 >>> anon_browser.open('http://launchpad.dev/~zzz')
17 >>> print extract_text(
18 ... find_tag_by_id(anon_browser.contents, 'portlet-map'))
190
=== removed file 'lib/lp/registry/stories/location/team-map.txt'
--- lib/lp/registry/stories/location/team-map.txt 2011-12-28 17:03:06 +0000
+++ lib/lp/registry/stories/location/team-map.txt 1970-01-01 00:00:00 +0000
@@ -1,86 +0,0 @@
1The map of a team's members
2===========================
3
4The map depends on a stream of XML-formatted data, giving the locations of
5all members of the team. We show that this stream works for teams with, and
6without, mapped members.
7
8 >>> anon_browser.open('http://launchpad.dev/~guadamen/+mapdata')
9 >>> print anon_browser.contents
10 <?xml version="1.0"...
11 <participants>
12 <!--
13 This collection of team location data is (c) Canonical Ltd and
14 contributors, and the format may be changed at any time.
15 -->
16 <participant
17 displayname="Colin Watson"
18 name="kamion"
19 logo_html="&lt;img alt=&quot;&quot;
20 width=&quot;64&quot; height=&quot;64&quot;
21 src=&quot;/@@/person-logo&quot; /&gt;"
22 url="/~kamion"
23 local_time="..."
24 lat="52.2"
25 lng="0.3"/>
26 </participants>
27 <BLANKLINE>
28
29 >>> anon_browser.open('http://launchpad.dev/~name21/+mapdata')
30 >>> print anon_browser.contents
31 <?xml version="1.0"...
32 <participants>
33 <!--
34 This collection of team location data is (c) Canonical Ltd and
35 contributors, and the format may be changed at any time.
36 -->
37 </participants>
38 <BLANKLINE>
39
40The team index page has a map with data limited to 24 members and uses
41a slightly different URL. We can see the URL works though there isn't
42enough test data to demonstrate the limit but that is done in the view
43test.
44
45 >>> anon_browser.open('http://launchpad.dev/~guadamen/+mapdataltd')
46 >>> print anon_browser.contents
47 <?xml version="1.0"...
48 <participants>
49 <!--
50 This collection of team location data is (c) Canonical Ltd and
51 contributors, and the format may be changed at any time.
52 -->
53 <participant
54 displayname="Colin Watson"
55 name="kamion"
56 logo_html="&lt;img alt=&quot;&quot;
57 width=&quot;64&quot; height=&quot;64&quot;
58 src=&quot;/@@/person-logo&quot; /&gt;"
59 url="/~kamion"
60 local_time="..."
61 lat="52.2"
62 lng="0.3"/>
63 </participants>
64 <BLANKLINE>
65
66
67+mapdata
68--------
69
70The display name of all team participants will be escaped to prevent
71XSS attacks on any callsite of +mapdata.
72
73 >>> from lp.testing.layers import MemcachedLayer
74
75 >>> admin_browser.open('http://launchpad.dev/~kamion/+edit')
76 >>> admin_browser.getControl('Display Name').value = (
77 ... "<script>alert('Colin \"nasty\"');</script>")
78 >>> admin_browser.getControl('Save Changes').click()
79
80 >>> MemcachedLayer.purge()
81 >>> anon_browser.open('http://launchpad.dev/~guadamen/+mapdata')
82 >>> print anon_browser.contents
83 <?xml version="1.0"...
84 ...displayname="&amp;lt;script&amp;gt;alert('Colin
85 &amp;quot;nasty&amp;quot;');&amp;lt;/script&amp;gt;"
86 ...
870
=== modified file 'lib/lp/registry/stories/person/xx-person-home.txt'
--- lib/lp/registry/stories/person/xx-person-home.txt 2011-12-18 13:45:20 +0000
+++ lib/lp/registry/stories/person/xx-person-home.txt 2012-02-17 12:13:23 +0000
@@ -141,12 +141,16 @@
141Summary Pagelets141Summary Pagelets
142----------------142----------------
143143
144A person's homepage also lists Karma information:144A person's homepage also lists Karma and Time zone information:
145145
146 >>> browser.open('http://launchpad.dev/~mark')146 >>> browser.open('http://launchpad.dev/~mark')
147 >>> print extract_text(find_tag_by_id(browser.contents, 'karma'))147 >>> print extract_text(find_tag_by_id(browser.contents, 'karma'))
148 Karma: 130 Karma help148 Karma: 130 Karma help
149149
150 >>> browser.open('http://launchpad.dev/~kamion')
151 >>> print extract_text(find_tag_by_id(browser.contents, 'timezone'))
152 Time zone: Europe/London (UTC+0000)
153
150Negative Ubuntu Code of Conduct signatory status is only displayed for154Negative Ubuntu Code of Conduct signatory status is only displayed for
151yourself; others won't see it:155yourself; others won't see it:
152156
@@ -173,25 +177,6 @@
173 2005-06-06177 2005-06-06
174178
175179
176Time zones
177..........
178
179The user's time zone is displayed next to their location details:
180
181 >>> browser.open('http://launchpad.dev/~kamion')
182 >>> print extract_text(
183 ... find_tag_by_id(browser.contents, 'portlet-map'))
184 Location
185 Time zone: Europe/London...
186
187If the user does not have location data set then the portlet will not be
188shown.
189
190 >>> browser.open('http://launchpad.dev/~bac')
191 >>> print extract_text(
192 ... find_tag_by_id(browser.contents, 'portlet-map'))
193
194
195Table of contributions180Table of contributions
196----------------------181----------------------
197182
198183
=== modified file 'lib/lp/registry/stories/webservice/xx-personlocation.txt'
--- lib/lp/registry/stories/webservice/xx-personlocation.txt 2009-10-01 12:30:32 +0000
+++ lib/lp/registry/stories/webservice/xx-personlocation.txt 2012-02-17 12:13:23 +0000
@@ -1,7 +1,9 @@
1= Person location =1= Person location =
22
3The location of a person is readable through the Web Service API, and can3The location of a person is readable through the Web Service API, and can
4be set that way, too.4be set that way too, but it has been deprecated as we no longer have that
5information in our database, so the latitude/longitude will always be None.
6The time_zone attribute has not been deprecated, though.
57
6We start with the case where there is no information about the user's8We start with the case where there is no information about the user's
7location, at all.9location, at all.
@@ -14,30 +16,8 @@
14 >>> print jdub['longitude']16 >>> print jdub['longitude']
15 None17 None
1618
17We show that we handle the case where there is no latitude or19It is also possible to set the location, but as you can see the
18longitude information.20latitude/longitude read via the Web API will still be None.
19
20 >>> marilize = webservice.get("/~marilize").jsonBody()
21 >>> print marilize['time_zone']
22 Africa/Maseru
23 >>> print marilize['latitude']
24 None
25 >>> print marilize['longitude']
26 None
27
28And here is an example, where we have location and time zone information for
29a user:
30
31 >>> kamion = webservice.get("/~kamion").jsonBody()
32 >>> print kamion['time_zone']
33 Europe/London
34 >>> print kamion['latitude']
35 52.2
36 >>> print kamion['longitude']
37 0.3
38
39It is also possible to set the location, if it is not yet locked by being
40provided by the user themselves.
4121
42 >>> print webservice.get("/~jdub").jsonBody()['time_zone']22 >>> print webservice.get("/~jdub").jsonBody()['time_zone']
43 None23 None
@@ -48,3 +28,7 @@
48 ...28 ...
49 >>> webservice.get("/~jdub").jsonBody()['time_zone']29 >>> webservice.get("/~jdub").jsonBody()['time_zone']
50 u'Australia/Sydney'30 u'Australia/Sydney'
31 >>> print jdub['latitude']
32 None
33 >>> print jdub['longitude']
34 None
5135
=== modified file 'lib/lp/registry/templates/person-index.pt'
--- lib/lp/registry/templates/person-index.pt 2011-06-30 12:58:22 +0000
+++ lib/lp/registry/templates/person-index.pt 2012-02-17 12:13:23 +0000
@@ -96,8 +96,6 @@
9696
97 </div>97 </div>
98 </div>98 </div>
99
100 <div tal:content="structure context/@@+portlet-map" />
101 </tal:is-valid-person>99 </tal:is-valid-person>
102100
103 <div id="not-lp-user-or-team"101 <div id="not-lp-user-or-team"
104102
=== modified file 'lib/lp/registry/templates/person-portlet-contact-details.pt'
--- lib/lp/registry/templates/person-portlet-contact-details.pt 2011-11-26 04:03:29 +0000
+++ lib/lp/registry/templates/person-portlet-contact-details.pt 2012-02-17 12:13:23 +0000
@@ -169,7 +169,7 @@
169 </dd>169 </dd>
170 </dl>170 </dl>
171171
172 <dl tal:condition="context/time_zone">172 <dl id="timezone" tal:condition="context/time_zone">
173 <dt>Time zone:173 <dt>Time zone:
174 <a tal:replace="structure overview_menu/editlocation/fmt:icon" />174 <a tal:replace="structure overview_menu/editlocation/fmt:icon" />
175 </dt>175 </dt>
176176
=== removed file 'lib/lp/registry/templates/person-portlet-map.pt'
--- lib/lp/registry/templates/person-portlet-map.pt 2010-09-21 03:30:43 +0000
+++ lib/lp/registry/templates/person-portlet-map.pt 1970-01-01 00:00:00 +0000
@@ -1,21 +0,0 @@
1<tal:root
2 xmlns:tal="http://xml.zope.org/namespaces/tal"
3 xmlns:metal="http://xml.zope.org/namespaces/metal"
4 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
5 omit-tag="">
6
7<div class="portlet" id="portlet-map"
8 tal:define="overview_menu context/menu:overview">
9
10 <tal:show-map condition="view/should_show_map_portlet">
11 <h2>Location</h2>
12
13 <div tal:condition="context/time_zone">
14 <strong>Time zone:</strong>
15 <span tal:replace="context/time_zone">UTC</span>
16 <a tal:replace="structure overview_menu/editlocation/fmt:icon" />
17 </div>
18 </tal:show-map>
19
20</div>
21</tal:root>
220
=== removed file 'lib/lp/registry/templates/team-map-data.pt'
--- lib/lp/registry/templates/team-map-data.pt 2010-06-24 20:22:57 +0000
+++ lib/lp/registry/templates/team-map-data.pt 1970-01-01 00:00:00 +0000
@@ -1,17 +0,0 @@
1<?xml version="1.0"?><!--*- mode: nxml -*-->
2<participants xmlns:tal="http://xml.zope.org/namespaces/tal"
3 tal:content="cache:public, 30 minute">
4 <!--
5 This collection of team location data is (c) Canonical Ltd and
6 contributors, and the format may be changed at any time.
7 -->
8 <participant tal:repeat="participant view/mapped_participants"
9 tal:attributes="
10 lat participant/latitude;
11 lng participant/longitude;
12 displayname participant/fmt:displayname/fmt:escape;
13 local_time participant/fmt:local-time;
14 logo_html participant/image:logo;
15 url participant/fmt:url;
16 name participant/name" />
17</participants>
180
=== removed file 'lib/lp/registry/templates/team-map.pt'
--- lib/lp/registry/templates/team-map.pt 2010-05-17 17:29:08 +0000
+++ lib/lp/registry/templates/team-map.pt 1970-01-01 00:00:00 +0000
@@ -1,45 +0,0 @@
1<html
2 xmlns="http://www.w3.org/1999/xhtml"
3 xmlns:tal="http://xml.zope.org/namespaces/tal"
4 xmlns:metal="http://xml.zope.org/namespaces/metal"
5 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
6 metal:use-macro="view/macro:page/main_only"
7 i18n:domain="launchpad"
8>
9
10<body>
11
12<div metal:fill-slot="main">
13
14 <div style="height:420px;">
15 <div tal:condition="not: view/has_mapped_participants">
16 None of the current participants in
17 <tal:teamname replace="context/fmt:displayname" /> have a recorded
18 location.
19 </div>
20
21 <div
22 tal:content="cache:public, 1 hour">
23 <tal:some_mapped condition="view/has_mapped_participants">
24 <div id="team_map_div"
25 style="width: 75%; height: 250px; border: 1px; float: left;
26 margin: 0 14px 0 0;">
27 </div>
28 <tal:map replace="structure view/map_html" />
29 </tal:some_mapped>
30
31 <div tal:condition="view/has_mapped_participants" style="float:left;">
32 <div style="margin-bottom: 4px;">
33 <strong>Members' current local time:</strong>
34 </div>
35 <tal:time_set repeat="time view/times">
36 <tal:times replace="time" /><br />
37 </tal:time_set>
38 </div>
39 </div>
40 </div>
41
42</div>
43
44</body>
45</html>
460
=== removed file 'lib/lp/registry/templates/team-portlet-map.pt'
--- lib/lp/registry/templates/team-portlet-map.pt 2010-09-21 04:21:16 +0000
+++ lib/lp/registry/templates/team-portlet-map.pt 1970-01-01 00:00:00 +0000
@@ -1,37 +0,0 @@
1<tal:root
2 xmlns:tal="http://xml.zope.org/namespaces/tal"
3 xmlns:metal="http://xml.zope.org/namespaces/metal"
4 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
5 omit-tag="">
6
7<div class="portlet" id="portlet-map" style="margin-bottom: 0px;"
8 tal:define="link context/menu:overview/map">
9<table>
10<tr><td>
11 <h2>
12 <span class="see-all">
13 <a tal:attributes="href link/fmt:url">View map and time zones</a>
14 </span>
15 Location
16 </h2>
17
18 <tal:can_view condition="context/required:launchpad.View">
19 <div style="width: 400px;" tal:condition="view/has_mapped_participants">
20 <div id="team_map_actions"
21 style="position:relative; z-index: 9999;
22 float:right; width: 8.5em; margin: 2px;
23 background-color: white; padding-bottom:1px;"></div>
24 <div id="team_map_div" class="small-map"
25 style="height: 200px; border: 1px; margin-top: 4px;"></div>
26 <tal:mapscript replace="structure view/map_portlet_html" />
27 </div>
28
29 <tal:no-map tal:condition="not: view/has_mapped_participants">
30 <a tal:attributes="href link/target"
31 ><img src="/+icing/portlet-map-unknown.png" /></a>
32 </tal:no-map>
33 </tal:can_view>
34</td></tr>
35</table>
36</div>
37</tal:root>
380
=== modified file 'lib/lp/registry/tests/test_person.py'
--- lib/lp/registry/tests/test_person.py 2012-02-16 08:27:37 +0000
+++ lib/lp/registry/tests/test_person.py 2012-02-17 12:13:23 +0000
@@ -718,8 +718,7 @@
718 'all_members_prepopulated', 'approvedmembers',718 'all_members_prepopulated', 'approvedmembers',
719 'deactivatedmembers', 'expiredmembers', 'inactivemembers',719 'deactivatedmembers', 'expiredmembers', 'inactivemembers',
720 'invited_members', 'member_memberships', 'pendingmembers',720 'invited_members', 'member_memberships', 'pendingmembers',
721 'proposedmembers', 'unmapped_participants', 'longitude',721 'proposedmembers', 'time_zone',
722 'latitude', 'time_zone',
723 )722 )
724 snap = Snapshot(self.myteam, providing=providedBy(self.myteam))723 snap = Snapshot(self.myteam, providing=providedBy(self.myteam))
725 for name in omitted:724 for name in omitted:
726725
=== modified file 'lib/lp/registry/tests/test_personset.py'
--- lib/lp/registry/tests/test_personset.py 2012-02-09 20:23:49 +0000
+++ lib/lp/registry/tests/test_personset.py 2012-02-17 12:13:23 +0000
@@ -32,7 +32,6 @@
32from lp.registry.interfaces.person import (32from lp.registry.interfaces.person import (
33 IPersonSet,33 IPersonSet,
34 PersonCreationRationale,34 PersonCreationRationale,
35 PersonVisibility,
36 TeamEmailAddressError,35 TeamEmailAddressError,
37 )36 )
38from lp.registry.interfaces.personnotification import IPersonNotificationSet37from lp.registry.interfaces.personnotification import IPersonNotificationSet
@@ -140,7 +139,7 @@
140 person.is_valid_person139 person.is_valid_person
141 person.karma140 person.karma
142 person.is_ubuntu_coc_signer141 person.is_ubuntu_coc_signer
143 person.location142 person.location,
144 person.archive143 person.archive
145 person.preferredemail144 person.preferredemail
146 self.assertThat(recorder, HasQueryCount(LessThan(1)))145 self.assertThat(recorder, HasQueryCount(LessThan(1)))
147146
=== modified file 'lib/lp/registry/xmlrpc/canonicalsso.py'
--- lib/lp/registry/xmlrpc/canonicalsso.py 2012-02-14 09:28:26 +0000
+++ lib/lp/registry/xmlrpc/canonicalsso.py 2012-02-17 12:13:23 +0000
@@ -38,7 +38,7 @@
38 if person is None:38 if person is None:
39 return39 return
4040
41 time_zone = person.location and person.location.time_zone41 time_zone = person.time_zone
42 team_names = dict(42 team_names = dict(
43 (removeSecurityProxy(t).name, t.private)43 (removeSecurityProxy(t).name, t.private)
44 for t in person.teams_participated_in)44 for t in person.teams_participated_in)