~launchpad-pqm/launchpad/devel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
Location widget
===============

A widget used when setting the geographic location of a given person.

    >>> from canonical.launchpad.webapp.servers import LaunchpadTestRequest
    >>> from lp.app.widgets.location import LocationWidget
    >>> from lp.registry.interfaces.person import IPersonSet
    >>> from lp.services.fields import LocationField
    >>> salgado = getUtility(IPersonSet).getByName('salgado')
    >>> field = LocationField(__name__='location', title=u'Location')

    >>> bound_field = field.bind(salgado)
    >>> request = LaunchpadTestRequest(
    ...     # Let's pretend requests are coming from Brazil.
    ...     environ={'REMOTE_ADDR': '201.13.165.145'})

When rendering salgado's location widget to himself, we'll center the
map around the location where he seems to be, based on the IP address of
the request.

    >>> login_person(salgado)
    >>> widget = LocationWidget(bound_field, request)
    >>> widget.zoom
    7
    >>> widget.center_lat
    -23...
    >>> widget.center_lng
    -45...

Since salgado's location hasn't been specified yet, there'll be no
visual marker indicating where he is in the map.

    >>> widget.show_marker
    0

If someone else sees salgado's location widget, though, we have no way
of guessing what coordinates to center the map around (because the
request doesn't come from salgado's browser), so we won't try to do so.

    >>> login(ANONYMOUS)
    >>> widget = LocationWidget(bound_field, request)
    >>> widget.zoom
    2
    >>> widget.center_lat
    15.0
    >>> widget.center_lng
    0.0
    >>> widget.show_marker
    0

When rendering the location widget of someone who already specified a
location, we'll obviously center the map around that, but we'll also put
a visual marker in the latitude/longitude specified as that person's
location.

    >>> kamion = getUtility(IPersonSet).getByName('kamion')
    >>> bound_field = field.bind(kamion)
    >>> login_person(kamion)
    >>> widget = LocationWidget(bound_field, request)
    >>> widget.zoom
    9
    >>> widget.center_lat
    52...
    >>> widget.center_lng
    0.29...
    >>> widget.show_marker
    1

The widget's getInputValue() method will return a LocationValue object,
which stored the geographic coordinates and the timezone.

    >>> request = LaunchpadTestRequest(
    ...     form={'field.location.time_zone': 'Europe/London',
    ...           'field.location.latitude': '51.3',
    ...           'field.location.longitude': '0.32'})
    >>> widget = LocationWidget(bound_field, request)
    >>> widget.hasInput()
    True
    >>> location = widget.getInputValue()
    >>> location
    <lp.app.widgets.location.LocationValue...
    >>> location.latitude # Only the integral part due to rounding errors.
    51...
    >>> location.longitude
    0.32...
    >>> location.time_zone
    'Europe/London'

If we try to set only the latitude *or* longitude, but not both, we'll
get an error.

    >>> request = LaunchpadTestRequest(
    ...     form={'field.location.time_zone': 'Europe/London',
    ...           'field.location.longitude': '0.32'})
    >>> widget = LocationWidget(bound_field, request)
    >>> widget.hasInput()
    True
    >>> widget.getInputValue()
    Traceback (most recent call last):
    ...
    WidgetInputError:...Please provide both latitude and longitude...

We also get errors if we don't specify a time zone or if the
latitude/longitude have non-realistic values.

    >>> request = LaunchpadTestRequest(
    ...     form={'field.location.latitude': '51.3',
    ...           'field.location.longitude': '0.32'})
    >>> widget = LocationWidget(bound_field, request)
    >>> widget.hasInput()
    True
    >>> location = widget.getInputValue()
    Traceback (most recent call last):
    ...
    MissingInputError: ('field.location.time_zone'...

    >>> request = LaunchpadTestRequest(
    ...     form={'field.location.time_zone': 'Europe/London',
    ...           'field.location.latitude': '99.3',
    ...           'field.location.longitude': '0.32'})
    >>> widget = LocationWidget(bound_field, request)
    >>> widget.hasInput()
    True
    >>> widget.getInputValue()
    Traceback (most recent call last):
    ...
    WidgetInputError:...Please provide a more realistic latitude and
    longitude...


The widget's HTML
-----------------

The widget's HTML will include <input> elements for the latitude,
longitude and time zone fields.  The values of these elements will be
set from within Javascript whenever the user changes the location on
the map.

    >>> bound_field = field.bind(kamion)
    >>> request = LaunchpadTestRequest(
    ...     environ={'REMOTE_ADDR': '201.13.165.145'})
    >>> login_person(kamion)
    >>> widget = LocationWidget(bound_field, request)
    >>> print widget()
    <input...name="field.location.latitude"...type="hidden"...
    <input...name="field.location.longitude"...type="hidden"...
    <select...name="field.location.time_zone"...


The widget's script
-------------------

The widget initializes the mapping.renderPersonMap() JavaScript methods
with its map_javascript property. The widget uses the
mapping.setLocation() method to lookup the time zone for the location.
The launchpad.geonames_identity config key provides the identity required to
access ba-ws.geonames.net's service.

    >>> from canonical.config import config

    >>> config.push('geoname_data', """
    ...     [launchpad]
    ...     geonames_identity: totoro
    ...     """)
    >>> print widget.map_javascript
    <BLANKLINE>
    <script type="text/javascript">
        LPS.use('node', 'lp.app.mapping', function(Y) {
            function renderMap() {
                Y.lp.app.mapping.renderPersonMap(
                    52.2, 0.3, "Colin Watson",
                    'kamion', '<img ... />', 'totoro',
                    'field.location.latitude', 'field.location.longitude',
                    'field.location.time_zone', 9, 1);
                }
            Y.on("domready", renderMap);
            });
    </script>

    # Restore the config the the default state.
    >>> config_data = config.pop('geoname_data')


XSS
---

The widget must escape and JS encode the person's displayname to prevent
XSS attacks and to make sure the generated javascript can be parsed.

    >>> kamion.displayname = (
    ...     "<script>alert('John \"nasty\" O\'Brien');</script>")
    >>> bound_field = field.bind(kamion)
    >>> widget = LocationWidget(bound_field, request)
    >>> print widget.map_javascript
    <BLANKLINE>
    ... "&lt;script&gt;alert('John &quot;nasty&quot; O'Brien');&lt;/script&gt;",...