0001-maps-add-handling-of-marker-behaviour-on-click-21034.patch
combo/apps/maps/migrations/0006_auto_20180215_1020.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 |
from __future__ import unicode_literals |
|
3 | ||
4 |
from django.db import migrations, models |
|
5 | ||
6 | ||
7 |
class Migration(migrations.Migration): |
|
8 | ||
9 |
dependencies = [ |
|
10 |
('maps', '0005_auto_20180212_1742'), |
|
11 |
] |
|
12 | ||
13 |
operations = [ |
|
14 |
migrations.AddField( |
|
15 |
model_name='map', |
|
16 |
name='marker_onclick_behaviour', |
|
17 |
field=models.CharField(default=b'none', max_length=32, verbose_name='Marker behaviour on clik', choices=[(b'none', 'Nothing'), (b'display_data', 'Display data in popup')]), |
|
18 |
), |
|
19 |
] |
combo/apps/maps/models.py | ||
---|---|---|
18 | 18 | |
19 | 19 |
from django.core import serializers |
20 | 20 |
from django.db import models |
21 |
from django.utils.html import escape |
|
21 | 22 |
from django.utils.text import slugify |
22 | 23 |
from django.utils.translation import ugettext_lazy as _ |
23 | 24 |
from django.core.urlresolvers import reverse_lazy |
... | ... | |
67 | 68 |
('wheelchair', _('Wheelchair')), |
68 | 69 |
] |
69 | 70 | |
71 |
MARKER_ONCLIK_BEHAVIOUR = [ |
|
72 |
('none', _('Nothing')), |
|
73 |
('display_data', _('Display data in popup')), |
|
74 |
] |
|
75 | ||
70 | 76 |
ZOOM_LEVELS = [ ('0', _('Whole world')), |
71 | 77 |
('9', _('Wide area')), |
72 | 78 |
('11', _('Area')), |
... | ... | |
76 | 82 |
('19', _('Ant')),] |
77 | 83 | |
78 | 84 | |
85 |
def escape_properties(properties): |
|
86 |
if isinstance(properties, (list,)): |
|
87 |
escaped = [] |
|
88 |
for prop in properties: |
|
89 |
if isinstance(prop, (list,)): |
|
90 |
prop = escape_properties(prop) |
|
91 |
escaped.append(prop) |
|
92 |
else: |
|
93 |
escaped.append(escape(prop)) |
|
94 |
else: |
|
95 |
escaped = {} |
|
96 |
for key, value in properties.iteritems(): |
|
97 |
if isinstance(value, (dict,)): |
|
98 |
value = escape_properties(value) |
|
99 |
escaped[escape(key)] = escape(value) |
|
100 |
return escaped |
|
101 | ||
102 | ||
79 | 103 |
class MapLayerManager(models.Manager): |
80 | 104 |
def get_by_natural_key(self, slug): |
81 | 105 |
return self.get(slug=slug) |
... | ... | |
172 | 196 |
features = [x for x in features if match(x)] |
173 | 197 | |
174 | 198 |
for feature in features: |
199 |
properties = feature.get('properties', {}) |
|
200 |
display_fields = properties.get('display_fields') |
|
201 |
if display_fields: |
|
202 |
feature['properties']['display_fields'] = escape_properties(display_fields) |
|
203 |
else: |
|
204 |
feature['properties'] = escape_properties(properties) |
|
205 | ||
175 | 206 |
feature['properties']['layer'] = { |
176 | 207 |
'colour': self.marker_colour, |
177 | 208 |
'icon_colour': self.icon_colour, |
... | ... | |
201 | 232 |
max_zoom = models.CharField(_('Maximal zoom level'), max_length=2, |
202 | 233 |
choices=ZOOM_LEVELS, default=19) |
203 | 234 |
group_markers = models.BooleanField(_('Group markers in clusters'), default=False) |
235 |
marker_onclick_behaviour = models.CharField(_('Marker behaviour on clik'), max_length=32, |
|
236 |
default='none', choices=MARKER_ONCLIK_BEHAVIOUR) |
|
204 | 237 |
layers = models.ManyToManyField(MapLayer, verbose_name=_('Layers'), blank=True) |
205 | 238 | |
206 | 239 |
template_name = 'maps/map_cell.html' |
... | ... | |
218 | 251 | |
219 | 252 |
def get_default_form_class(self): |
220 | 253 |
fields = ('title', 'initial_state', 'initial_zoom', 'min_zoom', |
221 |
'max_zoom', 'group_markers', 'layers') |
|
254 |
'max_zoom', 'group_markers', 'marker_onclick_behaviour', 'layers')
|
|
222 | 255 |
widgets = {'layers': forms.widgets.CheckboxSelectMultiple} |
223 | 256 |
return forms.models.modelform_factory(self.__class__, fields=fields, |
224 | 257 |
widgets=widgets) |
... | ... | |
248 | 281 |
ctx['map_attribution'] = settings.COMBO_MAP_ATTRIBUTION |
249 | 282 |
ctx['max_bounds'] = settings.COMBO_MAP_MAX_BOUNDS |
250 | 283 |
ctx['group_markers'] = self.group_markers |
284 |
ctx['marker_onclick_behaviour'] = self.marker_onclick_behaviour |
|
251 | 285 |
return ctx |
combo/apps/maps/static/js/combo.map.js | ||
---|---|---|
1 | 1 |
$(function() { |
2 | ||
2 | 3 |
L.Map.include( |
3 | 4 |
{ |
4 | 5 |
add_geojson_layer: function(callback) { |
... | ... | |
16 | 17 |
var geo_json = L.geoJson(data, { |
17 | 18 |
onEachFeature: function(feature, layer) { |
18 | 19 |
$(cell).trigger('combo:map-feature-prepare', {'feature': feature, 'layer': layer}); |
20 |
var marker_onclick_behaviour = $map_widget.data('marker-onclick-behaviour'); |
|
21 |
if (marker_onclick_behaviour === 'display_data') { |
|
22 |
var popup = ''; |
|
23 |
if (feature.properties.display_fields) { |
|
24 |
$.each(feature.properties.display_fields, function(key, value) { |
|
25 |
popup += '<p class="popup-field"><span class="field-value">' + value[1] + '</span>'; |
|
26 |
}); |
|
27 |
} else { |
|
28 |
var properties = feature.properties; |
|
29 |
$.each(Object.keys(properties).sort(), function(idx, key) { |
|
30 |
// exclude object type properties |
|
31 |
if (typeof(properties[key]) !== 'object') { |
|
32 |
popup += '<p class="popup-field"><span class="field-value">' + properties[key] + '</span>' + '</p>'; |
|
33 |
} |
|
34 |
}); |
|
35 |
} |
|
36 |
layer.bindPopup(popup); |
|
37 |
} |
|
19 | 38 |
}, |
20 | 39 |
pointToLayer: function (feature, latlng) { |
21 | 40 |
var markerStyles = "background-color: " + feature.properties.layer.colour + ";"; |
combo/apps/maps/templates/maps/map_cell.html | ||
---|---|---|
7 | 7 |
data-init-lng="{{ init_lng }}" data-geojson-url="{{ geojson_url }}" |
8 | 8 |
data-tile-urltemplate="{{ tile_urltemplate}}" data-map-attribution="{{ map_attribution}}" |
9 | 9 |
{% if group_markers %}data-group-markers="1"{% endif %} |
10 |
data-marker-onclick-behaviour="{{ cell.marker_onclick_behaviour }}" |
|
10 | 11 |
{% if max_bounds.corner1.lat %} |
11 | 12 |
data-max-bounds-lat1="{{ max_bounds.corner1.lat }}" |
12 | 13 |
data-max-bounds-lng1="{{ max_bounds.corner1.lng }}" |
combo/apps/maps/views.py | ||
---|---|---|
16 | 16 | |
17 | 17 |
import json |
18 | 18 | |
19 |
from django.views.generic.base import View |
|
20 | 19 |
from django.http import HttpResponse, Http404, HttpResponseForbidden |
20 |
from django.views.generic.base import View |
|
21 | 21 | |
22 | 22 |
from .models import Map |
23 | 23 |
tests/test_maps_cells.py | ||
---|---|---|
8 | 8 |
from django.test import Client |
9 | 9 |
from django.core.urlresolvers import reverse |
10 | 10 |
from django.contrib.auth.models import Group |
11 |
from django.utils.html import escape |
|
11 | 12 | |
12 | 13 |
from combo.data.models import Page |
13 |
from combo.apps.maps.models import MapLayer, Map |
|
14 |
from combo.apps.maps.models import MapLayer, Map, escape_properties
|
|
14 | 15 | |
15 | 16 |
pytestmark = pytest.mark.django_db |
16 | 17 | |
... | ... | |
225 | 226 |
assert len(json.loads(resp.content)['features']) == 1 |
226 | 227 |
assert 'orig=combo' in requests_get.call_args[0][1] |
227 | 228 |
assert not 'email=admin%40localhost&' in requests_get.call_args[0][1] |
229 | ||
230 | ||
231 |
def test_feature_properties_escaping(): |
|
232 |
list_properties = [ |
|
233 |
['code', '<script>alert("hello")</script>'], |
|
234 |
['category', 'potholes'], |
|
235 |
] |
|
236 |
escaped_list_properties = escape_properties(list_properties) |
|
237 |
assert escaped_list_properties[0][0] == 'code' |
|
238 |
assert escaped_list_properties[0][1] == escape(list_properties[0][1]) |
|
239 |
assert escaped_list_properties[1][0] == 'category' |
|
240 |
assert escaped_list_properties[1][1] == 'potholes' |
|
241 |
dict_properties = { |
|
242 |
'code': '<script>alert("hello")</script>', |
|
243 |
'category': 'potholes' |
|
244 |
} |
|
245 |
escaped_dict_properties = escape_properties(dict_properties) |
|
246 |
for key, value in escaped_dict_properties.items(): |
|
247 |
assert key in ('code', 'category') |
|
248 |
assert value in (escape(dict_properties['code']), 'potholes') |
|
228 |
- |