Projet

Général

Profil

0005-maps-define-tiles-layers-with-opacity-22639.patch

Lauréline Guérin, 18 février 2020 14:32

Télécharger (19 ko)

Voir les différences:

Subject: [PATCH 5/5] maps: define tiles layers with opacity (#22639)

 combo/apps/maps/forms.py                      | 19 +++++-
 combo/apps/maps/manager_views.py              | 39 +++++++++++
 .../maps/migrations/0010_map_layer_opacity.py | 20 ++++++
 combo/apps/maps/models.py                     | 47 ++++++++++++--
 combo/apps/maps/static/js/combo.map.js        | 19 ++++--
 combo/apps/maps/templates/maps/map_cell.html  | 12 +++-
 .../maps/templates/maps/map_cell_form.html    |  3 +
 combo/apps/maps/urls.py                       |  3 +
 tests/test_maps_cells.py                      | 64 +++++++++++++++----
 tests/test_maps_manager.py                    | 14 ++++
 10 files changed, 211 insertions(+), 29 deletions(-)
 create mode 100644 combo/apps/maps/migrations/0010_map_layer_opacity.py
combo/apps/maps/forms.py
73 73
class MapLayerOptionsForm(forms.ModelForm):
74 74
    class Meta:
75 75
        model = MapLayerOptions
76
        fields = ['map_layer']
76
        fields = ['map_layer', 'opacity']
77
        widgets = {
78
            'opacity': forms.NumberInput(attrs={'step': 0.1, 'min': 0, 'max': 1})
79
        }
77 80

  
78 81
    def __init__(self, *args, **kwargs):
79 82
        self.kind = kwargs.pop('kind')
80 83
        super(MapLayerOptionsForm, self).__init__(*args, **kwargs)
84
        # if edition, no possibility to change the layer
85
        if self.instance.pk:
86
            del self.fields['map_layer']
87
        else:
88
            if self.kind == 'geojson':
89
                self.fields['map_layer'].queryset = self.instance.map_cell.get_free_geojson_layers()
90
            else:
91
                self.fields['map_layer'].queryset = self.instance.map_cell.get_free_tiles_layers()
92
        # init opacity field only for tiles layers
81 93
        if self.kind == 'geojson':
82
            self.fields['map_layer'].queryset = self.instance.map_cell.get_free_geojson_layers()
94
            del self.fields['opacity']
83 95
        else:
84
            self.fields['map_layer'].queryset = self.instance.map_cell.get_free_tiles_layers()
96
            self.fields['opacity'].required = True
97
            self.fields['opacity'].initial = 1
combo/apps/maps/manager_views.py
94 94
map_cell_add_layer = MapCellAddLayer.as_view()
95 95

  
96 96

  
97
class MapCellEditLayer(UpdateView):
98
    form_class = MapLayerOptionsForm
99
    template_name = 'maps/layer_options_form.html'
100

  
101
    def dispatch(self, request, *args, **kwargs):
102
        try:
103
            self.cell = CellBase.get_cell(kwargs['cell_reference'], page=kwargs['page_pk'])
104
        except Map.DoesNotExist:
105
            raise Http404
106
        self.object = get_object_or_404(
107
            MapLayerOptions,
108
            pk=kwargs['layeroptions_pk'],
109
            map_cell=self.cell)
110
        return super(MapCellEditLayer, self).dispatch(request, *args, **kwargs)
111

  
112
    def get_object(self, *args, **kwargs):
113
        return self.object
114

  
115
    def get_form_kwargs(self):
116
        kwargs = super(MapCellEditLayer, self).get_form_kwargs()
117
        kwargs['kind'] = self.object.map_layer.kind
118
        return kwargs
119

  
120
    def form_valid(self, form):
121
        PageSnapshot.take(
122
            self.cell.page,
123
            request=self.request,
124
            comment=_('changed options of layer "%s" in cell "%s"') % (form.instance.map_layer, self.cell))
125
        return super(MapCellEditLayer, self).form_valid(form)
126

  
127
    def get_success_url(self):
128
        return '%s#cell-%s' % (
129
            reverse('combo-manager-page-view', kwargs={'pk': self.kwargs.get('page_pk')}),
130
            self.kwargs['cell_reference'])
131

  
132

  
133
map_cell_edit_layer = MapCellEditLayer.as_view()
134

  
135

  
97 136
class MapCellDeleteLayer(DeleteView):
98 137
    template_name = 'combo/generic_confirm_delete.html'
99 138

  
combo/apps/maps/migrations/0010_map_layer_opacity.py
1
# -*- coding: utf-8 -*-
2
from __future__ import unicode_literals
3

  
4
import django.core.validators
5
from django.db import migrations, models
6

  
7

  
8
class Migration(migrations.Migration):
9

  
10
    dependencies = [
11
        ('maps', '0009_map_layer_kind'),
12
    ]
13

  
14
    operations = [
15
        migrations.AddField(
16
            model_name='maplayeroptions',
17
            name='opacity',
18
            field=models.FloatField(null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)]),
19
        ),
20
    ]
combo/apps/maps/models.py
17 17
import json
18 18

  
19 19
from django.core import serializers
20
from django.core import validators
20 21
from django.db import models
21 22
from django.utils import six
22 23
from django.utils.encoding import python_2_unicode_compatible
......
333 334
    def is_enabled(cls):
334 335
        return MapLayer.objects.exists()
335 336

  
337
    def get_tiles_layers(self):
338
        tiles_layers = []
339
        options_qs = (
340
            self.maplayeroptions_set
341
            .filter(map_layer__kind='tiles')
342
            .select_related('map_layer')
343
            .order_by('-opacity'))
344
        for options in options_qs:
345
            tiles_layers.append({
346
                'tile_urltemplate': options.map_layer.tiles_template_url,
347
                'map_attribution': options.map_layer.tiles_attribution,
348
                'opacity': options.opacity or 0,
349
            })
350
        # check if at least one layer with opacity set to 1 exists
351
        if any([l['opacity'] == 1 for l in tiles_layers]):
352
            return tiles_layers
353
        # add the default tiles layer
354
        default_tiles_layer = MapLayer.get_default_tiles_layer()
355
        if default_tiles_layer is not None:
356
            tiles_layers.insert(0, {
357
                'tile_urltemplate': default_tiles_layer.tiles_template_url,
358
                'map_attribution': default_tiles_layer.tiles_attribution,
359
                'opacity': 1,
360
            })
361
        else:
362
            tiles_layers.insert(0, {
363
                'tile_urltemplate': settings.COMBO_MAP_TILE_URLTEMPLATE,
364
                'map_attribution': settings.COMBO_MAP_ATTRIBUTION,
365
                'opacity': 1,
366
            })
367
        return tiles_layers
368

  
336 369
    def get_cell_extra_context(self, context):
337 370
        ctx = super(Map, self).get_cell_extra_context(context)
338 371
        ctx['title'] = self.title
......
344 377
        ctx['min_zoom'] = self.min_zoom
345 378
        ctx['max_zoom'] = self.max_zoom
346 379
        ctx['geojson_url'] = reverse_lazy('mapcell-geojson', kwargs={'cell_id': self.pk})
347
        default_tiles_layer = MapLayer.get_default_tiles_layer()
348
        if default_tiles_layer is not None:
349
            ctx['tile_urltemplate'] = default_tiles_layer.tiles_template_url
350
            ctx['map_attribution'] = default_tiles_layer.tiles_attribution
351
        else:
352
            ctx['tile_urltemplate'] = settings.COMBO_MAP_TILE_URLTEMPLATE
353
            ctx['map_attribution'] = settings.COMBO_MAP_ATTRIBUTION
380
        ctx['tiles_layers'] = self.get_tiles_layers()
354 381
        ctx['max_bounds'] = settings.COMBO_MAP_MAX_BOUNDS
355 382
        ctx['group_markers'] = self.group_markers
356 383
        ctx['marker_behaviour_onclick'] = self.marker_behaviour_onclick
......
400 427
class MapLayerOptions(models.Model):
401 428
    map_cell = models.ForeignKey(Map, on_delete=models.CASCADE, db_column='map_id')
402 429
    map_layer = models.ForeignKey(MapLayer, verbose_name=_('Layer'), on_delete=models.CASCADE, db_column='maplayer_id')
430
    opacity = models.FloatField(
431
        validators=[
432
            validators.MinValueValidator(0),
433
            validators.MaxValueValidator(1)],
434
        null=True
435
    )
403 436

  
404 437
    class Meta:
405 438
        db_table = 'maps_map_layers'
combo/apps/maps/static/js/combo.map.js
124 124
        map_options.zoomControl = false;
125 125
        var latlng = [$map_widget.data('init-lat'), $map_widget.data('init-lng')];
126 126
        var geojson_url = $map_widget.data('geojson-url');
127
        var map_tile_url = $map_widget.data('tile-urltemplate');
128
        var map_attribution = $map_widget.data('map-attribution');
129 127
        if ($map_widget.data('max-bounds-lat1')) {
130 128
          map_options.maxBounds = L.latLngBounds(
131 129
                  L.latLng($map_widget.data('max-bounds-lat1'), $map_widget.data('max-bounds-lng1')),
......
175 173
            });
176 174
        }
177 175

  
178
        L.tileLayer(map_tile_url,
179
            {
180
                attribution: map_attribution,
181
                maxZoom: map_options.maxZoom
182
            }).addTo(map);
176
        var map_id = $map_widget.data('cell-id');
177
        var tiles_layers = window['tiles_'+map_id];
178
        $.each(tiles_layers, function(idx, layer) {
179
            L.tileLayer(
180
                layer.tile_urltemplate,
181
                {
182
                    attribution: layer.map_attribution,
183
                    opacity: layer.opacity,
184
                    maxZoom: map_options.maxZoom
185
                }
186
            ).addTo(map);
187
        });
183 188
        if (geojson_url) {
184 189
            map.add_geojson_layer(function(geo_json) {
185 190
                var bounds = geo_json.getBounds();
combo/apps/maps/templates/maps/map_cell.html
6 6
        data-init-zoom="{{ initial_zoom }}" data-min-zoom="{{ min_zoom }}"
7 7
        data-max-zoom="{{ max_zoom }}" data-init-lat="{{ init_lat }}"
8 8
        data-init-lng="{{ init_lng }}" data-geojson-url="{{ geojson_url }}"
9
        data-tile-urltemplate="{{ tile_urltemplate}}" data-map-attribution="{{ map_attribution}}"
10 9
        data-include-geoloc-button="true"
11 10
        {% if group_markers %}data-group-markers="1"{% endif %}
12 11
        data-marker-behaviour-onclick="{{ cell.marker_behaviour_onclick }}"
......
16 15
        data-max-bounds-lat2="{{ max_bounds.corner2.lat }}"
17 16
        data-max-bounds-lng2="{{ max_bounds.corner2.lng }}"
18 17
        {% endif %}
18
        data-cell-id="{{ cell.pk }}"
19 19
      >
20 20
{% endlocalize %}
21
<script>
22
  var tiles_{{ cell.pk }} = [];
23
  {% for layer in tiles_layers %}
24
    tiles_{{ cell.pk }}.push({
25
      tile_urltemplate: {{ layer.tile_urltemplate|as_json|safe }},
26
      map_attribution: {{ layer.map_attribution|as_json|safe }},
27
      opacity: {{ layer.opacity|as_json|safe }}
28
    });
29
  {% endfor %}
30
</script>
21 31
</div>
22 32
{% endblock %}
combo/apps/maps/templates/maps/map_cell_form.html
11 11
    {% for option in options %}
12 12
    <li>
13 13
      <span>{{ option.map_layer.label }} {% if option.map_layer.kind == 'tiles' %}({{ option.map_layer.get_kind_display }}){% endif %}</span>
14
      {% if option.map_layer.kind == 'tiles' %}
15
      <a rel="popup" title="{% trans "Edit" %}" class="link-action-icon edit" href="{% url 'maps-manager-cell-edit-layer' page_pk=page.pk cell_reference=cell.get_reference layeroptions_pk=option.pk %}">{% trans "Edit" %}</a>
16
      {% endif %}
14 17
      <a rel="popup" title="{% trans "Delete" %}" class="link-action-icon delete" href="{% url 'maps-manager-cell-delete-layer' page_pk=page.pk cell_reference=cell.get_reference layeroptions_pk=option.pk %}">{% trans "Delete" %}</a>
15 18
    </li>
16 19
    {% endfor %}
combo/apps/maps/urls.py
32 32
    url(r'^pages/(?P<page_pk>\d+)/cell/(?P<cell_reference>[\w_-]+)/add-layer/(?P<kind>geojson|tiles)/$',
33 33
        manager_views.map_cell_add_layer,
34 34
        name='maps-manager-cell-add-layer'),
35
    url(r'^pages/(?P<page_pk>\d+)/cell/(?P<cell_reference>[\w_-]+)/layer/(?P<layeroptions_pk>\d+)/edit/$',
36
        manager_views.map_cell_edit_layer,
37
        name='maps-manager-cell-edit-layer'),
35 38
    url(r'^pages/(?P<page_pk>\d+)/cell/(?P<cell_reference>[\w_-]+)/layer/(?P<layeroptions_pk>\d+)/delete/$',
36 39
        manager_views.map_cell_delete_layer,
37 40
        name='maps-manager-cell-delete-layer'),
tests/test_maps_cells.py
137 137
    page.save()
138 138
    cell = Map(page=page, placeholder='content', order=0, title='Map with points')
139 139
    cell.save()
140
    MapLayerOptions.objects.create(map_cell=cell, map_layer=layer)
140
    options = MapLayerOptions.objects.create(map_cell=cell, map_layer=layer)
141 141
    context = {'request': RequestFactory().get('/')}
142 142
    rendered = cell.render(context)
143 143
    assert 'data-init-zoom="13"' in rendered
......
147 147
    assert 'data-init-lng="2.3233688436448574"' in rendered
148 148
    assert 'data-geojson-url="/ajax/mapcell/geojson/1/"' in rendered
149 149
    assert 'data-group-markers="1"' not in rendered
150
    assert 'data-tile-urltemplate="%s"' % tiles_layer.tiles_template_url in rendered
151
    assert 'data-map-attribution="%s"' % tiles_layer.tiles_attribution in rendered
152 150
    resp = app.get('/test_map_cell/')
153 151
    assert 'xstatic/leaflet.js' in resp.text
154 152
    assert 'js/combo.map.js' in resp.text
......
160 158
    rendered = cell.render(context)
161 159
    assert 'data-group-markers="1"' in rendered
162 160

  
161

  
162
def test_cell_tiles_layers(tiles_layer):
163
    page = Page.objects.create(title='xxx', slug='test_map_cell', template_name='standard')
164
    cell = Map.objects.create(page=page, placeholder='content', order=0, title='Map with points')
165

  
166
    # no tiles layer for this map, take default tiles layers, tiles_layer
167
    assert cell.get_tiles_layers() == [{
168
        'tile_urltemplate': tiles_layer.tiles_template_url,
169
        'map_attribution': tiles_layer.tiles_attribution,
170
        'opacity': 1,
171
    }]
172

  
173
    # tiles_layer is not set as default, fallback on settings
163 174
    tiles_layer.tiles_default = False
164 175
    tiles_layer.save()
165
    rendered = cell.render(context)
166
    assert 'data-tile-urltemplate="%s"' % settings.COMBO_MAP_TILE_URLTEMPLATE in rendered
167
    assert 'data-map-attribution="%s"' % escape(settings.COMBO_MAP_ATTRIBUTION) in rendered
168

  
169
    tiles_layer.delete()
170
    rendered = cell.render(context)
171
    assert 'data-tile-urltemplate="%s"' % settings.COMBO_MAP_TILE_URLTEMPLATE in rendered
172
    assert 'data-map-attribution="%s"' % escape(settings.COMBO_MAP_ATTRIBUTION) in rendered
176
    assert cell.get_tiles_layers() == [{
177
        'tile_urltemplate': settings.COMBO_MAP_TILE_URLTEMPLATE,
178
        'map_attribution': settings.COMBO_MAP_ATTRIBUTION,
179
        'opacity': 1,
180
    }]
181

  
182
    # add a tile layer to the map, with opacity 1
183
    options = MapLayerOptions.objects.create(map_cell=cell, map_layer=tiles_layer, opacity=1)
184
    assert cell.get_tiles_layers() == [{
185
        'tile_urltemplate': tiles_layer.tiles_template_url,
186
        'map_attribution': tiles_layer.tiles_attribution,
187
        'opacity': 1,
188
    }]
189

  
190
    # opacity is less than 1 => add default tiles layer, defined in settings
191
    options.opacity = 0.5
192
    options.save()
193
    assert cell.get_tiles_layers() == [{
194
        'tile_urltemplate': settings.COMBO_MAP_TILE_URLTEMPLATE,
195
        'map_attribution': settings.COMBO_MAP_ATTRIBUTION,
196
        'opacity': 1,
197
    }, {
198
        'tile_urltemplate': tiles_layer.tiles_template_url,
199
        'map_attribution': tiles_layer.tiles_attribution,
200
        'opacity': 0.5,
201
    }]
202

  
203
    # set tiles_layer as default => add tiles_layer
204
    tiles_layer.tiles_default = True
205
    tiles_layer.save()
206
    assert cell.get_tiles_layers() == [{
207
        'tile_urltemplate': tiles_layer.tiles_template_url,
208
        'map_attribution': tiles_layer.tiles_attribution,
209
        'opacity': 1,
210
    }, {
211
        'tile_urltemplate': tiles_layer.tiles_template_url,
212
        'map_attribution': tiles_layer.tiles_attribution,
213
        'opacity': 0.5,
214
    }]
173 215

  
174 216

  
175 217
def test_get_geojson_on_non_public_page(app, layer):
tests/test_maps_manager.py
219 219
    assert list(cell.get_free_tiles_layers()) == [tiles_layer]
220 220
    resp = resp.click(href='.*/add-layer/geojson/$')
221 221
    assert list(resp.context['form'].fields['map_layer'].queryset) == [layer]
222
    assert 'opacity' not in resp.context['form'].fields
222 223
    resp.forms[0]['map_layer'] = layer.pk
223 224
    resp = resp.forms[0].submit()
224 225
    assert resp.status_int == 302
......
229 230
    assert options.map_layer == layer
230 231

  
231 232
    resp = resp.follow()
233
    assert '/layer/%s/edit/' % options.pk not in resp.text
232 234
    assert list(cell.get_free_geojson_layers()) == []
233 235
    assert list(cell.get_free_tiles_layers()) == [tiles_layer]
234 236
    assert '/add-layer/geojson/' not in resp.text
......
244 246
    resp = resp.click(href='.*/add-layer/tiles/$')
245 247
    assert list(resp.context['form'].fields['map_layer'].queryset) == [tiles_layer]
246 248
    resp.forms[0]['map_layer'] = tiles_layer.pk
249
    resp.forms[0]['opacity'] = 1
247 250
    resp = resp.forms[0].submit()
248 251
    assert resp.status_int == 302
249 252
    assert resp.location.endswith('/manage/pages/%s/#cell-%s' % (page.pk, cell.get_reference()))
......
251 254
    options = MapLayerOptions.objects.get()
252 255
    assert options.map_cell == cell
253 256
    assert options.map_layer == tiles_layer
257
    assert options.opacity == 1
258

  
259
    resp = resp.follow()
260
    resp = resp.click(href='.*/layer/%s/edit/$' % options.pk)
261
    assert 'map_layer' not in resp.context['form'].fields
262
    resp.forms[0]['opacity'] = 0.5
263
    resp = resp.forms[0].submit()
264
    assert resp.status_int == 302
265
    assert resp.location.endswith('/manage/pages/%s/#cell-%s' % (page.pk, cell.get_reference()))
266
    options.refresh_from_db()
267
    assert options.opacity == 0.5
254 268

  
255 269
    resp = resp.follow()
256 270
    assert list(cell.get_free_geojson_layers()) == [layer]
257
-