diff --git a/combo/apps/maps/forms.py b/combo/apps/maps/forms.py index d8d8c260..3220ef63 100644 --- a/combo/apps/maps/forms.py +++ b/combo/apps/maps/forms.py @@ -73,12 +73,25 @@ class MapLayerForm(forms.ModelForm): class MapLayerOptionsForm(forms.ModelForm): class Meta: model = MapLayerOptions - fields = ['map_layer'] + fields = ['map_layer', 'opacity'] + widgets = { + 'opacity': forms.NumberInput(attrs={'step': 0.1, 'min': 0, 'max': 1}) + } def __init__(self, *args, **kwargs): self.kind = kwargs.pop('kind') super(MapLayerOptionsForm, self).__init__(*args, **kwargs) - if self.kind == 'geojson': - self.fields['map_layer'].queryset = self.instance.map_cell.get_free_geojson_layers() + # if edition, no possibility to change the layer + if self.instance.pk: + del self.fields['map_layer'] else: - self.fields['map_layer'].queryset = self.instance.map_cell.get_free_tiles_layers() + if self.kind == 'geojson': + self.fields['map_layer'].queryset = self.instance.map_cell.get_free_geojson_layers() + else: + self.fields['map_layer'].queryset = self.instance.map_cell.get_free_tiles_layers() + # init opacity field only for tiles layers + if self.kind == 'geojson': + del self.fields['opacity'] + else: + self.fields['opacity'].required = True + self.fields['opacity'].initial = 1 diff --git a/combo/apps/maps/manager_views.py b/combo/apps/maps/manager_views.py index dbeb520f..b7e1f36b 100644 --- a/combo/apps/maps/manager_views.py +++ b/combo/apps/maps/manager_views.py @@ -94,6 +94,45 @@ class MapCellAddLayer(CreateView): map_cell_add_layer = MapCellAddLayer.as_view() +class MapCellEditLayer(UpdateView): + form_class = MapLayerOptionsForm + template_name = 'maps/layer_options_form.html' + + def dispatch(self, request, *args, **kwargs): + try: + self.cell = CellBase.get_cell(kwargs['cell_reference'], page=kwargs['page_pk']) + except Map.DoesNotExist: + raise Http404 + self.object = get_object_or_404( + MapLayerOptions, + pk=kwargs['layeroptions_pk'], + map_cell=self.cell) + return super(MapCellEditLayer, self).dispatch(request, *args, **kwargs) + + def get_object(self, *args, **kwargs): + return self.object + + def get_form_kwargs(self): + kwargs = super(MapCellEditLayer, self).get_form_kwargs() + kwargs['kind'] = self.object.map_layer.kind + return kwargs + + def form_valid(self, form): + PageSnapshot.take( + self.cell.page, + request=self.request, + comment=_('changed options of layer "%s" in cell "%s"') % (form.instance.map_layer, self.cell)) + return super(MapCellEditLayer, self).form_valid(form) + + def get_success_url(self): + return '%s#cell-%s' % ( + reverse('combo-manager-page-view', kwargs={'pk': self.kwargs.get('page_pk')}), + self.kwargs['cell_reference']) + + +map_cell_edit_layer = MapCellEditLayer.as_view() + + class MapCellDeleteLayer(DeleteView): template_name = 'combo/generic_confirm_delete.html' diff --git a/combo/apps/maps/migrations/0010_map_layer_opacity.py b/combo/apps/maps/migrations/0010_map_layer_opacity.py new file mode 100644 index 00000000..db64b06d --- /dev/null +++ b/combo/apps/maps/migrations/0010_map_layer_opacity.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('maps', '0009_map_layer_kind'), + ] + + operations = [ + migrations.AddField( + model_name='maplayeroptions', + name='opacity', + field=models.FloatField( + help_text='Float value between 0 and 1', null=True, + validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)], verbose_name='Opacity'), + ), + ] diff --git a/combo/apps/maps/models.py b/combo/apps/maps/models.py index 1aeb2824..718a9d9b 100644 --- a/combo/apps/maps/models.py +++ b/combo/apps/maps/models.py @@ -17,6 +17,7 @@ import json from django.core import serializers +from django.core import validators from django.db import models from django.utils import six from django.utils.encoding import python_2_unicode_compatible @@ -333,6 +334,38 @@ class Map(CellBase): def is_enabled(cls): return MapLayer.objects.exists() + def get_tiles_layers(self): + tiles_layers = [] + options_qs = ( + self.maplayeroptions_set + .filter(map_layer__kind='tiles') + .select_related('map_layer') + .order_by('-opacity')) + for options in options_qs: + tiles_layers.append({ + 'tile_urltemplate': options.map_layer.tiles_template_url, + 'map_attribution': options.map_layer.tiles_attribution, + 'opacity': options.opacity or 0, + }) + # check if at least one layer with opacity set to 1 exists + if any([l['opacity'] == 1 for l in tiles_layers]): + return tiles_layers + # add the default tiles layer + default_tiles_layer = MapLayer.get_default_tiles_layer() + if default_tiles_layer is not None: + tiles_layers.insert(0, { + 'tile_urltemplate': default_tiles_layer.tiles_template_url, + 'map_attribution': default_tiles_layer.tiles_attribution, + 'opacity': 1, + }) + else: + tiles_layers.insert(0, { + 'tile_urltemplate': settings.COMBO_MAP_TILE_URLTEMPLATE, + 'map_attribution': settings.COMBO_MAP_ATTRIBUTION, + 'opacity': 1, + }) + return tiles_layers + def get_cell_extra_context(self, context): ctx = super(Map, self).get_cell_extra_context(context) ctx['title'] = self.title @@ -344,13 +377,7 @@ class Map(CellBase): ctx['min_zoom'] = self.min_zoom ctx['max_zoom'] = self.max_zoom ctx['geojson_url'] = reverse_lazy('mapcell-geojson', kwargs={'cell_id': self.pk}) - default_tiles_layer = MapLayer.get_default_tiles_layer() - if default_tiles_layer is not None: - ctx['tile_urltemplate'] = default_tiles_layer.tiles_template_url - ctx['map_attribution'] = default_tiles_layer.tiles_attribution - else: - ctx['tile_urltemplate'] = settings.COMBO_MAP_TILE_URLTEMPLATE - ctx['map_attribution'] = settings.COMBO_MAP_ATTRIBUTION + ctx['tiles_layers'] = self.get_tiles_layers() ctx['max_bounds'] = settings.COMBO_MAP_MAX_BOUNDS ctx['group_markers'] = self.group_markers ctx['marker_behaviour_onclick'] = self.marker_behaviour_onclick @@ -400,6 +427,14 @@ class Map(CellBase): class MapLayerOptions(models.Model): map_cell = models.ForeignKey(Map, on_delete=models.CASCADE, db_column='map_id') map_layer = models.ForeignKey(MapLayer, verbose_name=_('Layer'), on_delete=models.CASCADE, db_column='maplayer_id') + opacity = models.FloatField( + verbose_name=_('Opacity'), + validators=[ + validators.MinValueValidator(0), + validators.MaxValueValidator(1)], + null=True, + help_text=_('Float value between 0 and 1'), + ) class Meta: db_table = 'maps_map_layers' diff --git a/combo/apps/maps/static/js/combo.map.js b/combo/apps/maps/static/js/combo.map.js index e9cd0bba..1283e8d9 100644 --- a/combo/apps/maps/static/js/combo.map.js +++ b/combo/apps/maps/static/js/combo.map.js @@ -124,8 +124,6 @@ $(function() { map_options.zoomControl = false; var latlng = [$map_widget.data('init-lat'), $map_widget.data('init-lng')]; var geojson_url = $map_widget.data('geojson-url'); - var map_tile_url = $map_widget.data('tile-urltemplate'); - var map_attribution = $map_widget.data('map-attribution'); if ($map_widget.data('max-bounds-lat1')) { map_options.maxBounds = L.latLngBounds( L.latLng($map_widget.data('max-bounds-lat1'), $map_widget.data('max-bounds-lng1')), @@ -175,11 +173,18 @@ $(function() { }); } - L.tileLayer(map_tile_url, - { - attribution: map_attribution, - maxZoom: map_options.maxZoom - }).addTo(map); + var map_id = $map_widget.data('cell-id'); + var tiles_layers = window['tiles_'+map_id]; + $.each(tiles_layers, function(idx, layer) { + L.tileLayer( + layer.tile_urltemplate, + { + attribution: layer.map_attribution, + opacity: layer.opacity, + maxZoom: map_options.maxZoom + } + ).addTo(map); + }); if (geojson_url) { map.add_geojson_layer(function(geo_json) { var bounds = geo_json.getBounds(); diff --git a/combo/apps/maps/templates/maps/map_cell.html b/combo/apps/maps/templates/maps/map_cell.html index 880906e8..d16af414 100644 --- a/combo/apps/maps/templates/maps/map_cell.html +++ b/combo/apps/maps/templates/maps/map_cell.html @@ -6,7 +6,6 @@ data-init-zoom="{{ initial_zoom }}" data-min-zoom="{{ min_zoom }}" data-max-zoom="{{ max_zoom }}" data-init-lat="{{ init_lat }}" data-init-lng="{{ init_lng }}" data-geojson-url="{{ geojson_url }}" - data-tile-urltemplate="{{ tile_urltemplate}}" data-map-attribution="{{ map_attribution}}" data-include-geoloc-button="true" {% if group_markers %}data-group-markers="1"{% endif %} data-marker-behaviour-onclick="{{ cell.marker_behaviour_onclick }}" @@ -16,7 +15,18 @@ data-max-bounds-lat2="{{ max_bounds.corner2.lat }}" data-max-bounds-lng2="{{ max_bounds.corner2.lng }}" {% endif %} + data-cell-id="{{ cell.pk }}" > {% endlocalize %} + {% endblock %} diff --git a/combo/apps/maps/templates/maps/map_cell_form.html b/combo/apps/maps/templates/maps/map_cell_form.html index 983da251..ab5e2a15 100644 --- a/combo/apps/maps/templates/maps/map_cell_form.html +++ b/combo/apps/maps/templates/maps/map_cell_form.html @@ -11,6 +11,9 @@ {% for option in options %}
  • {{ option.map_layer.label }} {% if option.map_layer.kind == 'tiles' %}({{ option.map_layer.get_kind_display }}){% endif %} + {% if option.map_layer.kind == 'tiles' %} + {% trans "Edit" %} + {% endif %} {% trans "Delete" %}
  • {% endfor %} diff --git a/combo/apps/maps/urls.py b/combo/apps/maps/urls.py index c26f29cf..a24d083e 100644 --- a/combo/apps/maps/urls.py +++ b/combo/apps/maps/urls.py @@ -32,6 +32,9 @@ maps_manager_urls = [ url(r'^pages/(?P\d+)/cell/(?P[\w_-]+)/add-layer/(?Pgeojson|tiles)/$', manager_views.map_cell_add_layer, name='maps-manager-cell-add-layer'), + url(r'^pages/(?P\d+)/cell/(?P[\w_-]+)/layer/(?P\d+)/edit/$', + manager_views.map_cell_edit_layer, + name='maps-manager-cell-edit-layer'), url(r'^pages/(?P\d+)/cell/(?P[\w_-]+)/layer/(?P\d+)/delete/$', manager_views.map_cell_delete_layer, name='maps-manager-cell-delete-layer'), diff --git a/tests/test_maps_cells.py b/tests/test_maps_cells.py index 6032d80a..2d06c1c3 100644 --- a/tests/test_maps_cells.py +++ b/tests/test_maps_cells.py @@ -137,7 +137,7 @@ def test_cell_rendering(app, layer, tiles_layer): page.save() cell = Map(page=page, placeholder='content', order=0, title='Map with points') cell.save() - MapLayerOptions.objects.create(map_cell=cell, map_layer=layer) + options = MapLayerOptions.objects.create(map_cell=cell, map_layer=layer) context = {'request': RequestFactory().get('/')} rendered = cell.render(context) assert 'data-init-zoom="13"' in rendered @@ -147,8 +147,6 @@ def test_cell_rendering(app, layer, tiles_layer): assert 'data-init-lng="2.3233688436448574"' in rendered assert 'data-geojson-url="/ajax/mapcell/geojson/1/"' in rendered assert 'data-group-markers="1"' not in rendered - assert 'data-tile-urltemplate="%s"' % tiles_layer.tiles_template_url in rendered - assert 'data-map-attribution="%s"' % tiles_layer.tiles_attribution in rendered resp = app.get('/test_map_cell/') assert 'xstatic/leaflet.js' in resp.text assert 'js/combo.map.js' in resp.text @@ -160,16 +158,60 @@ def test_cell_rendering(app, layer, tiles_layer): rendered = cell.render(context) assert 'data-group-markers="1"' in rendered + +def test_cell_tiles_layers(tiles_layer): + page = Page.objects.create(title='xxx', slug='test_map_cell', template_name='standard') + cell = Map.objects.create(page=page, placeholder='content', order=0, title='Map with points') + + # no tiles layer for this map, take default tiles layers, tiles_layer + assert cell.get_tiles_layers() == [{ + 'tile_urltemplate': tiles_layer.tiles_template_url, + 'map_attribution': tiles_layer.tiles_attribution, + 'opacity': 1, + }] + + # tiles_layer is not set as default, fallback on settings tiles_layer.tiles_default = False tiles_layer.save() - rendered = cell.render(context) - assert 'data-tile-urltemplate="%s"' % settings.COMBO_MAP_TILE_URLTEMPLATE in rendered - assert 'data-map-attribution="%s"' % escape(settings.COMBO_MAP_ATTRIBUTION) in rendered + assert cell.get_tiles_layers() == [{ + 'tile_urltemplate': settings.COMBO_MAP_TILE_URLTEMPLATE, + 'map_attribution': settings.COMBO_MAP_ATTRIBUTION, + 'opacity': 1, + }] - tiles_layer.delete() - rendered = cell.render(context) - assert 'data-tile-urltemplate="%s"' % settings.COMBO_MAP_TILE_URLTEMPLATE in rendered - assert 'data-map-attribution="%s"' % escape(settings.COMBO_MAP_ATTRIBUTION) in rendered + # add a tile layer to the map, with opacity 1 + options = MapLayerOptions.objects.create(map_cell=cell, map_layer=tiles_layer, opacity=1) + assert cell.get_tiles_layers() == [{ + 'tile_urltemplate': tiles_layer.tiles_template_url, + 'map_attribution': tiles_layer.tiles_attribution, + 'opacity': 1, + }] + + # opacity is less than 1 => add default tiles layer, defined in settings + options.opacity = 0.5 + options.save() + assert cell.get_tiles_layers() == [{ + 'tile_urltemplate': settings.COMBO_MAP_TILE_URLTEMPLATE, + 'map_attribution': settings.COMBO_MAP_ATTRIBUTION, + 'opacity': 1, + }, { + 'tile_urltemplate': tiles_layer.tiles_template_url, + 'map_attribution': tiles_layer.tiles_attribution, + 'opacity': 0.5, + }] + + # set tiles_layer as default => add tiles_layer + tiles_layer.tiles_default = True + tiles_layer.save() + assert cell.get_tiles_layers() == [{ + 'tile_urltemplate': tiles_layer.tiles_template_url, + 'map_attribution': tiles_layer.tiles_attribution, + 'opacity': 1, + }, { + 'tile_urltemplate': tiles_layer.tiles_template_url, + 'map_attribution': tiles_layer.tiles_attribution, + 'opacity': 0.5, + }] def test_get_geojson_on_non_public_page(app, layer): diff --git a/tests/test_maps_manager.py b/tests/test_maps_manager.py index c7518781..ae35355d 100644 --- a/tests/test_maps_manager.py +++ b/tests/test_maps_manager.py @@ -219,6 +219,7 @@ def test_add_delete_layer(app, admin_user, layer, tiles_layer): assert list(cell.get_free_tiles_layers()) == [tiles_layer] resp = resp.click(href='.*/add-layer/geojson/$') assert list(resp.context['form'].fields['map_layer'].queryset) == [layer] + assert 'opacity' not in resp.context['form'].fields resp.forms[0]['map_layer'] = layer.pk resp = resp.forms[0].submit() assert resp.status_int == 302 @@ -229,6 +230,7 @@ def test_add_delete_layer(app, admin_user, layer, tiles_layer): assert options.map_layer == layer resp = resp.follow() + assert '/layer/%s/edit/' % options.pk not in resp.text assert list(cell.get_free_geojson_layers()) == [] assert list(cell.get_free_tiles_layers()) == [tiles_layer] assert '/add-layer/geojson/' not in resp.text @@ -244,6 +246,7 @@ def test_add_delete_layer(app, admin_user, layer, tiles_layer): resp = resp.click(href='.*/add-layer/tiles/$') assert list(resp.context['form'].fields['map_layer'].queryset) == [tiles_layer] resp.forms[0]['map_layer'] = tiles_layer.pk + resp.forms[0]['opacity'] = 1 resp = resp.forms[0].submit() assert resp.status_int == 302 assert resp.location.endswith('/manage/pages/%s/#cell-%s' % (page.pk, cell.get_reference())) @@ -251,6 +254,17 @@ def test_add_delete_layer(app, admin_user, layer, tiles_layer): options = MapLayerOptions.objects.get() assert options.map_cell == cell assert options.map_layer == tiles_layer + assert options.opacity == 1 + + resp = resp.follow() + resp = resp.click(href='.*/layer/%s/edit/$' % options.pk) + assert 'map_layer' not in resp.context['form'].fields + resp.forms[0]['opacity'] = 0.5 + resp = resp.forms[0].submit() + assert resp.status_int == 302 + assert resp.location.endswith('/manage/pages/%s/#cell-%s' % (page.pk, cell.get_reference())) + options.refresh_from_db() + assert options.opacity == 0.5 resp = resp.follow() assert list(cell.get_free_geojson_layers()) == [layer]