Projet

Général

Profil

0002-maps-map-cell-8454.patch

Voir les différences:

Subject: [PATCH 2/2] maps: map cell (#8454)

 combo/apps/maps/migrations/0002_map.py          |  40 +++++++++
 combo/apps/maps/models.py                       | 102 ++++++++++++++++++++++-
 combo/apps/maps/static/css/combo.map.css        |  35 ++++++++
 combo/apps/maps/static/js/combo.map.js          |  71 ++++++++++++++++
 combo/apps/maps/templates/maps/map_cell.html    |   3 +
 combo/apps/maps/templates/maps/map_widget.html  |   4 +
 combo/apps/maps/urls.py                         |   4 +
 combo/apps/maps/views.py                        |  35 ++++++++
 combo/manager/templates/combo/manager_base.html |   4 +
 combo/settings.py                               |  12 +++
 debian/control                                  |   1 +
 requirements.txt                                |   1 +
 setup.py                                        |   1 +
 tests/test_maps_cells.py                        | 105 ++++++++++++++++++++++++
 tests/test_maps_manager.py                      |   7 +-
 15 files changed, 420 insertions(+), 5 deletions(-)
 create mode 100644 combo/apps/maps/migrations/0002_map.py
 create mode 100644 combo/apps/maps/static/css/combo.map.css
 create mode 100644 combo/apps/maps/static/js/combo.map.js
 create mode 100644 combo/apps/maps/templates/maps/map_cell.html
 create mode 100644 combo/apps/maps/templates/maps/map_widget.html
 create mode 100644 combo/apps/maps/views.py
 create mode 100644 tests/test_maps_cells.py
combo/apps/maps/migrations/0002_map.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
        ('data', '0027_page_picture'),
11
        ('auth', '0006_require_contenttypes_0002'),
12
        ('maps', '0001_initial'),
13
    ]
14

  
15
    operations = [
16
        migrations.CreateModel(
17
            name='Map',
18
            fields=[
19
                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
20
                ('placeholder', models.CharField(max_length=20)),
21
                ('order', models.PositiveIntegerField()),
22
                ('slug', models.SlugField(verbose_name='Slug', blank=True)),
23
                ('extra_css_class', models.CharField(max_length=100, verbose_name='Extra classes for CSS styling', blank=True)),
24
                ('public', models.BooleanField(default=True, verbose_name='Public')),
25
                ('restricted_to_unlogged', models.BooleanField(default=False, verbose_name='Restrict to unlogged users')),
26
                ('last_update_timestamp', models.DateTimeField(auto_now=True)),
27
                ('title', models.CharField(max_length=150, verbose_name='Title', blank=True)),
28
                ('default_position', models.CharField(default=b'48.83369263315934;2.3233688436448574', max_length=128, null=True, verbose_name='Default position', blank=True)),
29
                ('initial_zoom', models.CharField(default=b'13', max_length=2, verbose_name='Initial zoom level', choices=[(b'0', 'Whole world'), (b'9', 'Wide area'), (b'11', 'Area'), (b'13', 'Town'), (b'16', 'Small road'), (b'19', 'Ant')])),
30
                ('min_zoom', models.CharField(default=b'0', max_length=2, verbose_name='Minimal zoom level', choices=[(b'0', 'Whole world'), (b'9', 'Wide area'), (b'11', 'Area'), (b'13', 'Town'), (b'16', 'Small road'), (b'19', 'Ant')])),
31
                ('max_zoom', models.CharField(default=19, max_length=2, verbose_name='Maximal zoom level', choices=[(b'0', 'Whole world'), (b'9', 'Wide area'), (b'11', 'Area'), (b'13', 'Town'), (b'16', 'Small road'), (b'19', 'Ant')])),
32
                ('groups', models.ManyToManyField(to='auth.Group', verbose_name='Groups', blank=True)),
33
                ('layers', models.ManyToManyField(to='maps.MapLayer', verbose_name='Layers', blank=True)),
34
                ('page', models.ForeignKey(to='data.Page')),
35
            ],
36
            options={
37
                'verbose_name': 'Map Cell',
38
            },
39
        ),
40
    ]
combo/apps/maps/models.py
17 17

  
18 18
from django.db import models
19 19
from django.utils.translation import ugettext_lazy as _
20
from django.core.urlresolvers import reverse_lazy
21
from django import forms
22
from django import template
23
from django.conf import settings
24
from django.core.exceptions import PermissionDenied
25

  
26
from combo.data.models import CellBase
27
from combo.data.library import register_cell_class
28

  
29
zoom_levels = [ ('0', _('Whole world')),
30
                ('9', _('Wide area')),
31
                ('11', _('Area')),
32
                ('13', _('Town')),
33
                ('16', _('Small road')),
34
                ('19', _('Ant')),]
20 35

  
21 36
icons = [
22 37
    ('fa-home', _('home')),
......
37 52
from combo.utils import requests
38 53

  
39 54

  
55
class MapWidget(forms.TextInput):
56
    template_name = 'maps/map_widget.html'
57

  
58
    def render(self, name, value, attrs):
59
        final_attrs = self.build_attrs(attrs, name=name, value=value,
60
                                       type='hidden')
61
        cell_form_template = template.loader.get_template(self.template_name)
62
        return cell_form_template.render(final_attrs)
63

  
64

  
40 65
class MapLayer(models.Model):
41 66
    label = models.CharField(_('Label'), max_length=128)
42 67
    geojson_url = models.URLField(_('Geojson URL'))
......
48 73
    def __unicode__(self):
49 74
        return self.label
50 75

  
51
    def get_geojson(self):
76
    def get_geojson(self, request):
52 77
        response = requests.get(self.geojson_url,
53 78
                            remote_service='auto',
79
                            user=request.user,
54 80
                            headers={'accept': 'application/json'})
55 81
        if not response.ok:
56 82
            return []
......
61 87
            features = data
62 88

  
63 89
        for feature in features:
64
            feature['display_fields'] = feature['properties'].copy()
90
            if 'display_fields' not in feature['properties']:
91
                display_fields = []
92
                for label, value in feature['properties'].iteritems():
93
                    if value is not None:
94
                        display_fields.append((label, value))
95
                feature['properties']['display_fields'] = display_fields
65 96
            feature['properties']['colour'] = self.marker_colour
66 97
            feature['properties']['icon_colour'] = self.icon_colour
67 98
            feature['properties']['label'] = self.label
68 99
            feature['properties']['icon'] = self.icon
69 100
        return features
101

  
102

  
103
@register_cell_class
104
class Map(CellBase):
105
    title = models.CharField(_('Title'), max_length=150, blank=True)
106
    default_position = models.CharField(_('Default position'), null=True, blank=True,
107
                            default='%(lat)s;%(lon)s' % settings.COMBO_MAP_DEFAULT_POSITION,
108
                            max_length=128)
109
    initial_zoom = models.CharField(_('Initial zoom level'), max_length=2,
110
                                    choices=zoom_levels, default='13')
111
    min_zoom = models.CharField(_('Minimal zoom level'), max_length=2,
112
                                   choices=zoom_levels, default='0')
113
    max_zoom = models.CharField(_('Maximal zoom level'), max_length=2,
114
                                choices=zoom_levels, default=19)
115
    layers = models.ManyToManyField(MapLayer, verbose_name=_('Layers'), blank=True)
116

  
117
    template_name = 'maps/map_cell.html'
118

  
119
    class Meta:
120
        verbose_name = _('Map')
121

  
122
    class Media:
123
        js = ('xstatic/leaflet.js', 'js/combo.map.js')
124
        css = {'all': ('xstatic/leaflet.css', 'xstatic/css/font-awesome.min.css',
125
                       'css/combo.map.css')}
126

  
127
    def get_default_form_class(self):
128
        fields = ('title', 'default_position', 'initial_zoom', 'min_zoom',
129
                  'max_zoom', 'layers')
130
        lat, lng = self.default_position.split(';')
131
        widgets = {'layers': forms.widgets.CheckboxSelectMultiple,
132
                   'default_position': MapWidget(attrs={'init_lat': lat,
133
                                                        'init_lng': lng,
134
                                                        'init_zoom': self.initial_zoom,
135
                                                        'min_zoom': self.min_zoom,
136
                                                        'max_zoom': self.max_zoom,
137
                                                        'tile_urltemplate': settings.COMBO_MAP_TILE_URLTEMPLATE,
138
                                                        'map_attribution': settings.COMBO_MAP_ATTRIBUTION})}
139
        return forms.models.modelform_factory(self.__class__, fields=fields,
140
                                             widgets=widgets)
141

  
142
    def get_geojson(self, request):
143
        geojson = {'type': 'FeatureCollection', 'features': []}
144
        if not self.page.is_visible(request.user):
145
            raise PermissionDenied()
146
        if not self.is_visible(request.user):
147
            raise PermissionDenied()
148
        for layer in self.layers.all():
149
            geojson['features'] += layer.get_geojson(request)
150
        return geojson
151

  
152

  
153
    @classmethod
154
    def is_enabled(cls):
155
        return MapLayer.objects.count() > 0
156

  
157
    def get_cell_extra_context(self, context):
158
        ctx = super(Map, self).get_cell_extra_context(context)
159
        ctx['title'] = self.title
160
        ctx['initial_lat'], ctx['initial_lng'] = self.default_position.split(';');
161
        ctx['initial_zoom'] = self.initial_zoom
162
        ctx['min_zoom'] = self.min_zoom
163
        ctx['max_zoom'] = self.max_zoom
164
        ctx['geojson_url'] = reverse_lazy('mapcell-geojson', kwargs={'cell_id': self.pk})
165
        ctx['tile_urltemplate'] = settings.COMBO_MAP_TILE_URLTEMPLATE
166
        ctx['map_attribution'] = settings.COMBO_MAP_ATTRIBUTION
167
        return ctx
combo/apps/maps/static/css/combo.map.css
1
div.combo-cell-map {
2
    height: 60vh;
3
}
4

  
5
/* leaflet styles */
6

  
7
div.leaflet-div-icon span {
8
    width: 2.3rem;
9
    height: 2.3rem;
10
    display: block;
11
    left: -1rem;
12
    top: -1rem;
13
    position: relative;
14
    border-radius: 11rem 6rem 0.8rem;
15
    transform: scale(1, 1.3) rotate(45deg);
16
    border: 1px solid #aaa;
17
}
18

  
19
div.leaflet-popup-content span {
20
    display: block;
21
}
22

  
23
div.leaflet-popup-content span.field-label {
24
    text-transform: capitalize;
25
}
26

  
27
div.leaflet-popup-content span.field-value {
28
    font-weight: bold;
29
}
30

  
31
div.leaflet-div-icon span i:before {
32
    display: inline-block;
33
    margin: 9px;
34
    transform: scale(1.1) rotate(-45deg);
35
}
combo/apps/maps/static/js/combo.map.js
1
$(function() {
2
    function render_map() {
3
        $('div.combo-cell-map').each(function() {
4
            var $map_widget = $(this);
5
            var map_options = Object();
6
            var initial_zoom = parseInt($map_widget.data('init-zoom'));
7
            if (! isNaN(initial_zoom)) {
8
                map_options.zoom = initial_zoom;
9
            } else {
10
                map_options.zoom = 13;
11
            }
12
            var max_zoom = parseInt($map_widget.data('max_zoom'));
13
            if (!isNaN(max_zoom)) map_options.maxZoom = max_zoom;
14
            var min_zoom = parseInt($map_widget.data('min-zoom'));
15
            if (!isNaN(min_zoom)) map_options.minZoom = min_zoom;
16
            var latlng = [$map_widget.data('init-lat'), $map_widget.data('init-lng')];
17
            var geojson_url = $map_widget.data('geojson-url');
18
            var map_tile_url = $map_widget.data('tile-urltemplate');
19
            var map_attribution = $map_widget.data('map-attribution');
20
            var map = L.map(this, map_options);
21
            var store_position_selector = $map_widget.data('store-position');
22
            map.setView(latlng, map_options.zoom);
23

  
24
            L.tileLayer(map_tile_url,
25
                {
26
                    attribution: map_attribution
27
                }).addTo(map);
28
            if (store_position_selector) {
29
                map.marker = L.marker(latlng);
30
                map.marker.addTo(map);
31
                var hidden = $('input#' + store_position_selector);
32
                map.on('click', function(e) {
33
                    map.marker.setLatLng(e.latlng);
34
                    hidden.val(e.latlng.lat + ';' + e.latlng.lng);
35
                });
36
            }
37
            if (geojson_url) {
38
                $.getJSON(geojson_url, function(data) {
39
                    var geo_json = L.geoJson(data, {
40
                        onEachFeature: function(feature, layer) {
41
                            if (feature.properties.display_fields) {
42
                                var popup = '';
43
                                $.each(feature.properties.display_fields, function(key, value) {
44
                                    popup += '<p class="popup-field"><span class="field-label">' + value[0] + '</span>';
45
                                    popup += '<span class="field-value">' + value[1] + '</span></p>';
46
                                });
47
                            } else {
48
                                var popup = '<p class="popup-field">' + feature.properties.label + '</p>';
49
                            }
50
                            layer.bindPopup(popup);
51
                        },
52
                        pointToLayer: function (feature, latlng) {
53
                            var markerStyles = "background-color: "+feature.properties.colour+";";
54
                            marker = L.divIcon({iconAnchor: [0, 30],
55
                                                popupAnchor: [5, -45],
56
                                                html: '<span style="' + markerStyles + '"><i class="fa '+feature.properties.icon+'" style="color:'+feature.properties.icon_colour+'"></i></span>'
57
                                               });
58
                            return L.marker(latlng, {icon: marker});
59
                        }
60
                    });
61
                    map.fitBounds(geo_json.getBounds());
62
                    geo_json.addTo(map);
63
                });
64
            }
65
        });
66
    }
67
    $('div.combo-cell-map').parents('div.cell').on('combo:cellform-reloaded', function() {
68
        render_map();
69
    });
70
    $('div.combo-cell-map').parents('div.cell').trigger('combo:cellform-reloaded');
71
});
combo/apps/maps/templates/maps/map_cell.html
1
<h2>{{ title }}</h2>
2
<div class="combo-cell-map" data-init-zoom="{{ initial_zoom }}" data-min-zoom="{{ min_zoom }}" data-max-zoom="{{ max_zoom }}" data-init-lat="{{ initial_lat }}" data-init-lng="{{ initial_lng }}" data-geojson-url="{{ geojson_url }}" data-tile-urltemplate="{{ tile_urltemplate}}" data-map-attribution="{{ map_attribution}}">
3
</div>
combo/apps/maps/templates/maps/map_widget.html
1
<div class="combo-cell-map" data-init-zoom="{{ init_zoom }}" data-min-zoom="{{ min_zoom }}" data-max-zoom="{{ max_zoom }}" data-init-lat="{{ init_lat }}" data-init-lng="{{ init_lng }}" data-store-position="combo-map-{{ id }}" data-tile-urltemplate="{{ tile_urltemplate}}" data-map-attribution="{{ map_attribution}}">
2
</div>
3

  
4
<input type="{{ type }}" name="{{ name }}" value="{{ value }}" id="combo-map-{{ id }}" />
combo/apps/maps/urls.py
21 21
from .manager_views import (ManagerHomeView, LayerAddView,
22 22
                LayerEditView, LayerDeleteView)
23 23

  
24
from .views import GeojsonView
25

  
24 26
maps_manager_urls = [
25 27
    url('^$', ManagerHomeView.as_view(), name='maps-manager-homepage'),
26 28
    url('^layers/add/$', LayerAddView.as_view(), name='maps-manager-layer-add'),
......
33 35
urlpatterns = [
34 36
    url(r'^manage/maps/', decorated_includes(manager_required,
35 37
        include(maps_manager_urls))),
38
    url(r'^ajax/mapcell/geojson/(?P<cell_id>\w+)/$', GeojsonView.as_view(),
39
        name='mapcell-geojson'),
36 40
]
combo/apps/maps/views.py
1
# combo - content management system
2
# Copyright (C) 2017  Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
import json
18

  
19
from django.views.generic.base import View
20
from django.http import HttpResponse, Http404
21

  
22
from .models import Map
23

  
24

  
25
class GeojsonView(View):
26

  
27
    def get(self, request, *args, **kwargs):
28
        try:
29
            cell = Map.objects.get(pk=kwargs['cell_id'])
30
        except Map.DoesNotExist:
31
            raise Http404()
32

  
33
        geojson = cell.get_geojson(request)
34
        content_type = 'application/json'
35
        return HttpResponse(json.dumps(geojson), content_type=content_type)
combo/manager/templates/combo/manager_base.html
3 3

  
4 4
{% block css %}
5 5
<link rel="stylesheet" type="text/css" media="all" href="{{ STATIC_URL }}css/combo.manager.css"/>
6
<link rel="stylesheet" type="text/css" media="all" href="{% static "xstatic/leaflet.css" %}"></script>
7
<link rel="stylesheet" type="text/css" media="all" href="{% static "css/combo.map.css" %}"></script>
6 8
{% endblock %}
7 9
{% block page-title %}{% firstof site_title "Combo" %}{% endblock %}
8 10
{% block site-title %}{% firstof site_title "Combo" %}{% endblock %}
......
30 32
<script src="{% static "ckeditor/ckeditor/ckeditor.js" %}"></script>
31 33
<script type="text/javascript" src="{% static "ckeditor/ckeditor-init.js" %}"></script>
32 34
<script src="{% static "js/combo.manager.js" %}"></script>
35
<script src="{% static "xstatic/leaflet.js" %}"></script>
36
<script src="{% static "js/combo.map.js" %}"></script>
33 37
{% endblock %}
combo/settings.py
81 81
    'haystack',
82 82
    'xstatic.pkg.chartnew_js',
83 83
    'xstatic.pkg.font_awesome',
84
    'xstatic.pkg.leaflet',
84 85
)
85 86

  
86 87
INSTALLED_APPS = plugins.register_plugins_apps(INSTALLED_APPS)
......
284 285
# dashboard support
285 286
COMBO_DASHBOARD_ENABLED = False
286 287

  
288
# default position on maps
289
COMBO_MAP_DEFAULT_POSITION = {'lat': '48.83369263315934',
290
                              'lon': '2.3233688436448574'
291
                              }
292

  
293
# default map tiles url
294
COMBO_MAP_TILE_URLTEMPLATE = 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
295

  
296
# default combo map attribution
297
COMBO_MAP_ATTRIBUTION = 'Map data &copy; <a href="https://openstreetmap.org">OpenStreetMap</a> contributors, <a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>'
298

  
287 299
local_settings_file = os.environ.get('COMBO_SETTINGS_FILE',
288 300
        os.path.join(os.path.dirname(__file__), 'local_settings.py'))
289 301
if os.path.exists(local_settings_file):
debian/control
17 17
    python-django-cmsplugin-blurp,
18 18
    python-xstatic-chartnew-js,
19 19
    python-xstatic-font-awesome,
20
    python-xstatic-leaflet,
20 21
    python-eopayment (>= 1.9),
21 22
    python-django-haystack (>= 2.4.0),
22 23
    python-sorl-thumbnail,
requirements.txt
7 7
requests
8 8
XStatic-ChartNew.js
9 9
XStatic-Font-Awesome
10
XStatic-Leaflet
10 11
eopayment>=1.13
11 12
python-dateutil
12 13
djangorestframework>=3.3, <3.4
setup.py
112 112
        'requests',
113 113
        'XStatic-ChartNew.js',
114 114
        'XStatic-Font-Awesome',
115
        'XStatic-Leaflet',
115 116
        'eopayment>=1.13',
116 117
        'python-dateutil',
117 118
        'djangorestframework>=3.3, <3.4',
tests/test_maps_cells.py
1
# -*- coding: utf-8 -*-
2
import pytest
3

  
4
from django.contrib.auth.models import User
5
from django.test.client import RequestFactory
6
from django.template import Context
7
from django.test import Client
8
from django.core.urlresolvers import reverse
9
from django.contrib.auth.models import Group
10

  
11
from combo.data.models import Page
12
from combo.apps.maps.models import MapLayer, Map
13

  
14
pytestmark = pytest.mark.django_db
15

  
16
client = Client()
17

  
18
@pytest.fixture
19
def user():
20
    try:
21
        user = User.objects.get(username='admin')
22
    except User.DoesNotExist:
23
        user = User.objects.create_user('admin', email=None, password='admin')
24
    return user
25

  
26
@pytest.fixture
27
def layer():
28
    try:
29
        layer = MapLayer.objects.get()
30
    except MapLayer.DoesNotExist:
31
        layer = MapLayer()
32
        layer.geojson_url = 'http://example.net/geojson'
33
        layer.marker_colour = 'FF0000'
34
        layer.icon = 'fa-bicycle'
35
        layer.icon_colour = '0000FF'
36
        layer.save()
37
    return layer
38

  
39
def login(username='admin', password='admin'):
40
    resp = client.post('/login/', {'username': username, 'password': password})
41
    assert resp.status_code == 302
42

  
43
def test_cell_disabled():
44
    MapLayer.objects.all().delete()
45
    assert Map.is_enabled() is False
46

  
47
def test_cell_enabled(layer):
48
    assert Map.is_enabled() is True
49

  
50
def test_cell_rendering(layer):
51
    page = Page(title='xxx', slug='test_map_cell', template_name='standard')
52
    page.save()
53
    cell = Map(page=page, placeholder='content', order=0,
54
                   title = 'Map with points')
55
    cell.save()
56
    cell.layers.add(layer)
57
    context = Context({'request': RequestFactory().get('/')})
58
    rendered = cell.render(context)
59
    assert 'data-init-zoom="13"' in rendered
60
    assert 'data-min-zoom="0"' in rendered
61
    assert 'data-max-zoom="19"' in rendered
62
    assert 'data-init-lat="48.83369263315934"' in rendered
63
    assert 'data-init-lng="2.3233688436448574"' in rendered
64
    assert 'data-geojson-url="/ajax/mapcell/geojson/1/"' in rendered
65
    resp = client.get('/test_map_cell/')
66
    assert 'xstatic/leaflet.js' in resp.content
67
    assert 'js/combo.map.js' in resp.content
68
    assert 'xstatic/leaflet.css' in resp.content
69
    assert 'xstatic/css/font-awesome.min.css' in resp.content
70
    assert 'css/combo.map.css' in resp.content
71

  
72

  
73
def test_get_geojson_on_non_public_page(layer):
74
    page = Page(title='xxx', slug='new', template_name='standard',
75
                public=False)
76
    page.save()
77
    cell = Map(page=page, placeholder='content', order=0,
78
                   title = 'Map with points')
79
    cell.save()
80
    cell.layers.add(layer)
81
    resp = client.get(reverse('mapcell-geojson', kwargs={'cell_id': cell.id}))
82
    assert resp.status_code == 403
83

  
84
def test_get_geojson_on_non_publik_cell(layer):
85
    page = Page(title='xxx', slug='new', template_name='standard')
86
    page.save()
87
    cell = Map(page=page, placeholder='content', order=0, public=False,
88
                   title = 'Map with points')
89
    cell.save()
90
    cell.layers.add(layer)
91
    resp = client.get(reverse('mapcell-geojson', kwargs={'cell_id': cell.id}))
92
    assert resp.status_code == 403
93

  
94
def test_geojson_on_restricted_cell(layer, user):
95
    page = Page(title='xxx', slug='new', template_name='standard')
96
    page.save()
97
    group = Group.objects.create(name='map tester')
98
    cell = Map(page=page, placeholder='content', order=0, public=False)
99
    cell.title = 'Map with points'
100
    cell.save()
101
    cell.layers.add(layer)
102
    cell.groups.add(group)
103
    login()
104
    resp = client.get(reverse('mapcell-geojson', kwargs={'cell_id': cell.id}))
105
    assert resp.status_code == 403
tests/test_maps_manager.py
89 89
        assert item['type'] == 'Feature'
90 90
        assert item['geometry']['type'] == 'Point'
91 91
        assert item['geometry']['coordinates'] == [2.3233688436448574, 48.83369263315934]
92
        assert item['display_fields'] == {'property': 'property value'}
92
        assert item['properties']['display_fields'] == [('property', 'property value')]
93 93
        assert item['properties']['icon'] == 'fa-bicycle'
94 94
        assert item['properties']['label'] == 'Test'
95 95
        assert item['properties']['colour'] == '#FFFFFF'
......
98 98
    mocked_response.json.return_value = {'type': 'FeatureCollection',
99 99
            'features': [{'geometry': {'type': 'Point',
100 100
                        'coordinates': [2.3233688436448574, 48.83369263315934]},
101
                        'properties': {'property': 'a random value'}}
101
                        'properties': {'property': 'a random value',
102
                                       'display_fields': [('foo', 'bar')]}}
102 103
            ]
103 104
    }
104 105
    mocked_response.ok.return_value = True
......
108 109
    for item in geojson:
109 110
        assert item['geometry']['type'] == 'Point'
110 111
        assert item['geometry']['coordinates'] == [2.3233688436448574, 48.83369263315934]
111
        assert item['display_fields'] == {'property': 'a random value'}
112
        assert item['properties']['display_fields'] == [('foo', 'bar')]
112 113
        assert item['properties']['icon'] == 'fa-bicycle'
113 114
        assert item['properties']['label'] == 'Test'
114 115
        assert item['properties']['colour'] == '#FFFFFF'
115
-