From d43d01d4ea96e097a240d24bae710f5dbfc8fd22 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20P=C3=A9ters?= <fpeters@entrouvert.com>
Date: Mon, 9 Nov 2020 09:44:43 +0100
Subject: [PATCH 3/3] fields: use new map marker selection widget in front
 (#47066)

---
 tests/form_pages/test_all.py                  |  55 +++++++++++++++++-
 tests/utilities.py                            |  12 ++++
 wcs/fields.py                                 |  27 ++++++---
 wcs/qommon/form.py                            |  27 +++++++++
 .../static/images/blank-marker-icon.png       | Bin 0 -> 1783 bytes
 wcs/qommon/static/js/qommon.map.js            |  55 +++++++++++++++++-
 .../forms/widgets/map-marker-selection.html   |  31 ++++++++++
 .../templates/qommon/forms/widgets/map.html   |   1 +
 8 files changed, 195 insertions(+), 13 deletions(-)
 create mode 100644 wcs/qommon/static/images/blank-marker-icon.png
 create mode 100644 wcs/qommon/templates/qommon/forms/widgets/map-marker-selection.html

diff --git a/tests/form_pages/test_all.py b/tests/form_pages/test_all.py
index be3386918..5ce99231d 100644
--- a/tests/form_pages/test_all.py
+++ b/tests/form_pages/test_all.py
@@ -9,7 +9,7 @@ import re
 import time
 import zipfile
 import base64
-from webtest import Upload, Hidden
+from webtest import Upload, Hidden, Radio
 import mock
 import xml.etree.ElementTree as ET
 
@@ -8957,3 +8957,56 @@ def test_structured_workflow_options(pub):
         '1_structured': {'id': '1', 'text': 'un', 'more': 'foo'},
     }
     assert '2020-04-18' in formdata.evolution[0].parts[0].content
+
+
+def test_form_item_map_data_source(pub, http_requests):
+    NamedDataSource.wipe()
+    data_source = NamedDataSource(name='foobar')
+    data_source.data_source = {
+        'type': 'geojson',
+        'value': 'http://remote.example.net/geojson',
+    }
+    data_source.id_property = 'id'
+    data_source.label_template_property = '{{ text }}'
+    data_source.cache_duration = '5'
+    data_source.store()
+
+    formdef = create_formdef()
+    formdef.fields = [
+        fields.ItemField(id='1', label='map', display_mode='map', data_source={'type': 'foobar'}),
+    ]
+    formdef.store()
+    formdef.data_class().wipe()
+    app = get_app(pub)
+    resp = app.get('/test/')
+    assert resp.pyquery('div[data-markers-radio-name]')[0].attrib['data-markers-url'] == '/api/geojson/foobar'
+    assert resp.pyquery('div[data-markers-radio-name]')[0].attrib['data-markers-radio-name'] == 'f1$marker_id'
+    app.get('/api/geojson/wrong-foobar', status=404)
+    resp_geojson = app.get('/api/geojson/foobar')
+    assert len(resp_geojson.json['features']) == 2
+    assert http_requests.count() == 1
+    assert http_requests.get_last('url') == 'http://remote.example.net/geojson'
+    resp_geojson = app.get('/api/geojson/foobar')
+    assert http_requests.count() == 1  # cache was used
+    assert len(resp_geojson.json['features']) == 2
+    # simulate qommon.map.js that will create radio inputs
+    resp.form.fields['f1$marker_id'] = [Radio(form=resp.form, tag='input', name='f1$marker_id', pos=5)]
+    resp.form.fields['f1$marker_id'][0].options.append(('1', False, None))
+    resp.form.fields['f1$marker_id'][0].options.append(('2', False, None))
+    resp.form.fields['f1$marker_id'][0].optionPositions.append(5)
+    resp.form.fields['f1$marker_id'][0].optionPositions.append(6)
+    resp.form.field_order.append(('f1$marker_id', resp.form.fields['f1$marker_id'][0]))
+    resp.form['f1$marker_id'].value = '1'  # click on marker
+    resp.form['f1$latlng'] = '1;2'  # set via js
+    resp = resp.form.submit('submit')
+    assert 'Check values then click submit.' in resp
+    # selected option is displayed as readonly:
+    assert resp.pyquery('input[type=text][value=foo][readonly]')
+    resp = resp.form.submit('submit')
+    resp = resp.follow()
+    assert 'The form has been recorded' in resp
+    assert '<div class="value">foo</div>' in resp
+    assert formdef.data_class().count() == 1
+    data_id = formdef.data_class().select()[0].id
+    formdata = formdef.data_class().get(data_id)
+    assert formdata.data['1_structured']['geometry']['coordinates'] == [1, 2]
diff --git a/tests/utilities.py b/tests/utilities.py
index 9ed741564..28568cd18 100644
--- a/tests/utilities.py
+++ b/tests/utilities.py
@@ -1,5 +1,6 @@
 import email.header
 import email.parser
+import json
 import os
 import tempfile
 import random
@@ -326,6 +327,16 @@ class HttpRequestsMocking(object):
 
         with open(os.path.join(os.path.dirname(__file__), 'idp_metadata.xml')) as fd:
             metadata = fd.read()
+        geojson = {
+            'features': [
+                {'properties': {'id': '1', 'text': 'foo'},
+                 'geometry': {'type': 'Point', 'coordinates': [1, 2]}
+                },
+                {'properties': {'id': '2', 'text': 'bar'},
+                 'geometry': {'type': 'Point', 'coordinates': [3, 4]}
+                }
+            ]
+        }
         status, data, headers = {
             'http://remote.example.net/204': (204, None, None),
             'http://remote.example.net/400': (400, 'bad request', None),
@@ -336,6 +347,7 @@ class HttpRequestsMocking(object):
             'http://remote.example.net/json': (200, '{"foo": "bar"}', None),
             'http://remote.example.net/json-list': (200, '{"data": [{"id": "a", "text": "b"}]}', None),
             'http://remote.example.net/json-list-extra': (200, '{"data": [{"id": "a", "text": "b", "foo": "bar"}]}', None),
+            'http://remote.example.net/geojson': (200, json.dumps(geojson), None),
             'http://remote.example.net/json-err0': (200, '{"data": "foo", "err": 0}', None),
             'http://remote.example.net/json-err1': (200, '{"data": "", "err": 1}', None),
             'http://remote.example.net/json-list-err1': (200, '{"data": [{"id": "a", "text": "b"}], "err": 1}', None),
diff --git a/wcs/fields.py b/wcs/fields.py
index 22fe06beb..8c75c62c6 100644
--- a/wcs/fields.py
+++ b/wcs/fields.py
@@ -1498,6 +1498,12 @@ class ItemField(WidgetField, MapOptionsMixin):
         if getattr(element.find('show_as_radio'), 'text', None) == 'True':
             self.display_mode = 'radio'
 
+    @property
+    def extra_attributes(self):
+        if self.display_mode == 'map':
+            return ['initial_zoom', 'min_zoom', 'max_zoom', 'data_source']
+        return []
+
     def get_options(self, mode=None):
         if self.data_source:
             return [x[:3] for x in data_sources.get_items(self.data_source, mode=mode)]
@@ -1531,15 +1537,16 @@ class ItemField(WidgetField, MapOptionsMixin):
             self.widget_class = JsonpSingleSelectWidget
             return
 
-        if self.data_source:
-            items = data_sources.get_items(self.data_source,
-                    include_disabled=self.display_disabled_items)
-            kwargs['options'] = [x[:3] for x in items if not x[-1].get('disabled')]
-            kwargs['options_with_attributes'] = items[:]
-        else:
-            kwargs['options'] = self.get_options()
-        if not kwargs.get('options'):
-            kwargs['options'] = [(None, '---')]
+        if self.display_mode != 'map':
+            if self.data_source:
+                items = data_sources.get_items(self.data_source,
+                        include_disabled=self.display_disabled_items)
+                kwargs['options'] = [x[:3] for x in items if not x[-1].get('disabled')]
+                kwargs['options_with_attributes'] = items[:]
+            else:
+                kwargs['options'] = self.get_options()
+            if not kwargs.get('options'):
+                kwargs['options'] = [(None, '---')]
         if display_mode == 'radio':
             self.widget_class = RadiobuttonsWidget
             if type(kwargs['options'][0]) is str:
@@ -1552,6 +1559,8 @@ class ItemField(WidgetField, MapOptionsMixin):
                 kwargs['delim'] = htmltext('<br />')
         elif display_mode == 'autocomplete':
             kwargs['select2'] = True
+        elif display_mode == 'map':
+            self.widget_class = MapMarkerSelectionWidget
 
     def get_display_value(self, value):
         data_source = data_sources.get_object(self.data_source)
diff --git a/wcs/qommon/form.py b/wcs/qommon/form.py
index 653b1eeb1..5ee293c50 100644
--- a/wcs/qommon/form.py
+++ b/wcs/qommon/form.py
@@ -2470,6 +2470,33 @@ class MapWidget(CompositeWidget):
             self.value = '%s;%s' % (lat_lon['lat'], lat_lon['lon'])
 
 
+class MapMarkerSelectionWidget(MapWidget):
+    template_name = 'qommon/forms/widgets/map-marker-selection.html'
+
+    def __init__(self, name, value=None, **kwargs):
+        CompositeWidget.__init__(self, name, value, **kwargs)
+        self.add(HiddenWidget, 'marker_id', value=value)
+        self.readonly = kwargs.pop('readonly', False)
+
+        self.map_attributes = {}
+        self.map_attributes.update(get_publisher().get_map_attributes())
+        self.sync_map_and_address_fields = get_publisher().has_site_option('sync-map-and-address-fields')
+        for attribute in ('initial_zoom', 'min_zoom', 'max_zoom', 'init_with_geoloc'):
+            if attribute in kwargs:
+                self.map_attributes['data-' + attribute] = kwargs.pop(attribute)
+
+        from wcs import data_sources
+        data_source = data_sources.get_object(kwargs['data_source'])
+        self.geojson_markers_url = data_source.get_geojson_url()
+
+    def initial_position(self):
+        return None
+
+    def _parse(self, request):
+        CompositeWidget._parse(self, request)
+        self.value = self.get('marker_id')
+
+
 class HiddenErrorWidget(HiddenWidget):
     def set_error(self, error):
         Widget.set_error(self, error)
diff --git a/wcs/qommon/static/images/blank-marker-icon.png b/wcs/qommon/static/images/blank-marker-icon.png
new file mode 100644
index 0000000000000000000000000000000000000000..830438ddb4889d415b35d93aca44572b3a2b7596
GIT binary patch
diff --git a/wcs/qommon/static/js/qommon.map.js b/wcs/qommon/static/js/qommon.map.js
index 595e239df..6f0babe4c 100644
--- a/wcs/qommon/static/js/qommon.map.js
+++ b/wcs/qommon/static/js/qommon.map.js
@@ -46,10 +46,14 @@ $(window).on('load', function() {
      var hidden = $(this).prev();
      map.marker = null;
      var latlng;
+     var initial_marker_id = $map_widget.data('markers-initial-id');
      if ($map_widget.data('init-lat')) {
        latlng = [$map_widget.data('init-lat'), $map_widget.data('init-lng')]
-       map.marker = L.marker(latlng);
-       map.marker.addTo(map);
+       if (typeof initial_marker_id == 'undefined') {
+         // if markers are used they will appear via their input widget
+         map.marker = L.marker(latlng);
+         map.marker.addTo(map);
+       }
      } else if ($map_widget.data('def-lat')) {
        latlng = [$map_widget.data('def-lat'), $map_widget.data('def-lng')]
      } else {
@@ -67,7 +71,52 @@ $(window).on('load', function() {
      } else {
        map.addControl(gps_control);
      }
-     if (! $map_widget.data('readonly')) {
+     if ($map_widget.data('markers-url')) {
+       var radio_name = $map_widget.data('markers-radio-name');
+       $map_widget.on('change', 'input[name="' + radio_name + '"]', function() {
+         var $radio = $(this);
+         if ($radio.is(':checked')) {
+           hidden.val($radio.data('lat') + ';' + $radio.data('lng'));
+           hidden.trigger('change');
+         }
+       });
+       $.getJSON($map_widget.data('markers-url')).done(
+         function(data) {
+           var geo_json = L.geoJson(data, {
+             pointToLayer: function (feature, latlng) {
+               var $label = $('<label>', {
+                 title: feature.properties._text
+               });
+               var $radio = $('<input>', {
+                 value: feature.properties._id,
+                 name: radio_name,
+                 type: 'radio',
+                 'data-lat': latlng.lat,
+                 'data-lng': latlng.lng
+               });
+               if (typeof initial_marker_id !== 'undefined' && feature.properties._id == initial_marker_id) {
+                 $radio.attr('checked', 'checked');
+               }
+               $label.append($radio);
+               $label.append($('<span></span>'));
+               var div_marker = L.divIcon({
+                 className: 'item-marker',
+                 html: '<div>' + $label.prop('outerHTML') + '</div>'
+               });
+               return L.marker(latlng, {icon: div_marker});
+             }
+           });
+           map.fitBounds(geo_json.getBounds());
+           geo_json.addTo(map);
+         }
+       );
+     } else if ($map_widget.data('readonly') && initial_marker_id) {
+       // readonly and marker
+         map.marker = L.marker(latlng);
+         map.marker.addTo(map);
+     }
+
+     if (! $map_widget.data('readonly') && ! $map_widget.data('markers-url')) {
        map.on('click', function(e) {
          $map_widget.trigger('set-geolocation', e.latlng);
        });
diff --git a/wcs/qommon/templates/qommon/forms/widgets/map-marker-selection.html b/wcs/qommon/templates/qommon/forms/widgets/map-marker-selection.html
new file mode 100644
index 000000000..fc0918541
--- /dev/null
+++ b/wcs/qommon/templates/qommon/forms/widgets/map-marker-selection.html
@@ -0,0 +1,31 @@
+{% extends "qommon/forms/widgets/map.html" %}
+
+{% block widget-control-attributes %}
+  {% if not widget.readonly %}
+    data-markers-url="{{ widget.geojson_markers_url }}"
+    data-markers-radio-name="{{widget.name}}$marker_id"
+  {% endif %}
+  {% if widget.value %}
+    data-markers-initial-id="{{ widget.value }}"
+  {% endif %}
+{% endblock %}
+
+{% block widget-control %}
+{{ block.super }}
+
+<style>
+.item-marker input + span {
+  position: relative;
+  z-index: 1000;
+  margin-top: -55px;
+  margin-left: -5px;
+  display: block;
+  width: 25px;
+  height: 41px;
+  background: url(/static/images/blank-marker-icon.png);
+}
+.item-marker input:checked + span {
+  background: url(/static/xstatic/images/marker-icon.png);
+}
+</style>
+{% endblock %}
diff --git a/wcs/qommon/templates/qommon/forms/widgets/map.html b/wcs/qommon/templates/qommon/forms/widgets/map.html
index a9a48d1bf..85375cdae 100644
--- a/wcs/qommon/templates/qommon/forms/widgets/map.html
+++ b/wcs/qommon/templates/qommon/forms/widgets/map.html
@@ -10,5 +10,6 @@
     data-init-lat="{{ widget.initial_position.lat }}"
     data-init-lng="{{ widget.initial_position.lng }}"
   {% endif %}
+  {% block widget-control-attributes %}{% endblock %}
 ></div>
 {% endblock %}
-- 
2.29.2

