From 4e633d68e44ea4fc2f2d302821e452ab13d03342 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20P=C3=A9ters?= <fpeters@entrouvert.com>
Date: Sat, 13 Apr 2019 18:06:02 +0200
Subject: [PATCH] forms: sync map with address fields (#27418)

---
 tests/test_api.py                             | 21 +++++++++++
 wcs/api.py                                    | 14 ++++++++
 wcs/qommon/form.py                            |  1 +
 wcs/qommon/static/js/qommon.geolocation.js    | 35 +++++++++++++++++++
 .../templates/qommon/forms/widgets/map.html   |  1 +
 wcs/urls.py                                   |  1 +
 6 files changed, 73 insertions(+)

diff --git a/tests/test_api.py b/tests/test_api.py
index fcee91558..938bbe740 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -2384,3 +2384,24 @@ def test_reverse_geocoding(pub):
         pub.site_options.write(open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w'))
         resp = get_app(pub).get('/api/reverse-geocoding?lat=0&lon=0')
         assert urlopen.call_args[0][0] == 'http://reverse.example.net/?param=value&format=json&addressdetails=1&lat=0&lon=0&accept-language=en'
+
+
+def test_geocoding(pub):
+    with mock.patch('qommon.misc.urlopen') as urlopen:
+        urlopen.side_effect = lambda *args: StringIO(json.dumps([{'lat': 0, 'lon': 0}]))
+        get_app(pub).get('/api/geocoding', status=400)
+        resp = get_app(pub).get('/api/geocoding?q=test')
+        assert resp.content_type == 'application/json'
+        assert resp.body == json.dumps([{'lat': 0, 'lon': 0}])
+        assert urlopen.call_args[0][0] == 'http://nominatim.openstreetmap.org/search?format=json&q=test&accept-language=en'
+
+        pub.site_options.add_section('options')
+        pub.site_options.set('options', 'nominatim_key', 'KEY')
+        pub.site_options.write(open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w'))
+        resp = get_app(pub).get('/api/geocoding?q=test')
+        assert urlopen.call_args[0][0] == 'http://nominatim.openstreetmap.org/search?key=KEY&format=json&q=test&accept-language=en'
+
+        pub.site_options.set('options', 'geocoding_service_url', 'http://reverse.example.net/?param=value')
+        pub.site_options.write(open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w'))
+        resp = get_app(pub).get('/api/geocoding?q=test')
+        assert urlopen.call_args[0][0] == 'http://reverse.example.net/?param=value&format=json&q=test&accept-language=en'
diff --git a/wcs/api.py b/wcs/api.py
index 9af5cdb3d..5b9fc4327 100644
--- a/wcs/api.py
+++ b/wcs/api.py
@@ -810,6 +810,20 @@ def reverse_geocoding(request, *args, **kwargs):
     url += '&accept-language=%s' % (get_publisher().get_site_language() or 'en')
     return HttpResponse(misc.urlopen(url).read(), content_type='application/json')
 
+def geocoding(request, *args, **kwargs):
+    if not 'q' in request.GET:
+        return HttpResponseBadRequest()
+    q = request.GET['q']
+    url = get_publisher().get_geocoding_service_url()
+    if '?' in url:
+        url += '&'
+    else:
+        url += '?'
+    url += 'format=json&q=%s' % urllib.quote(q.encode('utf-8'))
+    url += '&accept-language=%s' % (get_publisher().get_site_language() or 'en')
+    return HttpResponse(misc.urlopen(url).read(), content_type='application/json')
+
+
 def validate_expression(request, *args, **kwargs):
     expression = request.GET.get('expression')
     hint = {'klass': None, 'msg': ''}
diff --git a/wcs/qommon/form.py b/wcs/qommon/form.py
index 597eb80aa..f3099eeef 100644
--- a/wcs/qommon/form.py
+++ b/wcs/qommon/form.py
@@ -2109,6 +2109,7 @@ class MapWidget(CompositeWidget):
         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)
diff --git a/wcs/qommon/static/js/qommon.geolocation.js b/wcs/qommon/static/js/qommon.geolocation.js
index d2d22e497..8248d15e1 100644
--- a/wcs/qommon/static/js/qommon.geolocation.js
+++ b/wcs/qommon/static/js/qommon.geolocation.js
@@ -26,6 +26,7 @@ function geoloc_prefill(element_type, element_value)
 
   $(document).on('set-geolocation', function(event, coords) {
     $.getJSON(WCS_ROOT_URL + '/api/reverse-geocoding?lat=' + coords.lat + '&lon=' + coords.lng, function(data) {
+      unset_sync_callback()
       geoloc_prefill('house', data.address.house_number);
       var street = data.address.road;
       if (!street && data.address.pedestrian) {
@@ -41,6 +42,7 @@ function geoloc_prefill(element_type, element_value)
       geoloc_prefill('postcode', data.address.postcode);
       geoloc_prefill('city', data.address.village || data.address.town || data.address.city || data.address.county);
       geoloc_prefill('country', data.address.country);
+      set_sync_callback()
     });
   });
   if ($('.qommon-map').length == 0) {
@@ -62,4 +64,37 @@ function geoloc_prefill(element_type, element_value)
       );
     }
   }
+
+  function set_sync_callback() {
+    var $map = $('.qommon-map');
+    if (! $map.data('address-sync')) return;
+    $('div[data-geolocation]').on('change', 'input[type=text], textarea, select', function(event) {
+      var address = '';
+      $(['number-and-street', 'house', 'road', 'postcode', 'city', 'country']).each(function(idx, elem) {
+        var part = $('div[data-geolocation="' + elem + '"]').find('input, textarea, select').val();
+        if (part) {
+          address += part + ' ';
+          if (elem == 'number-and-street' || elem == 'road' || elem == 'city') {
+            address += ', ';
+          }
+        }
+      });
+      $.getJSON(WCS_ROOT_URL + '/api/geocoding?q=' + address, function(data) {
+        if (data && $(data).length > 0) {
+          var coords = {lat: data[0].lat, lon: data[0].lon};
+          var map = $map[0].leaflet_map;
+          map.flyTo(coords);
+          if (map.marker === null) {
+            map.marker = L.marker([0, 0]);
+            map.marker.addTo(map);
+          }
+          map.marker.setLatLng(coords);
+        }
+      });
+    });
+  }
+  function unset_sync_callback() {
+    $('div[data-geolocation]').off('change', 'input[type=text], textarea, select');
+  }
+  set_sync_callback();
 });
diff --git a/wcs/qommon/templates/qommon/forms/widgets/map.html b/wcs/qommon/templates/qommon/forms/widgets/map.html
index d61d1c191..a9a48d1bf 100644
--- a/wcs/qommon/templates/qommon/forms/widgets/map.html
+++ b/wcs/qommon/templates/qommon/forms/widgets/map.html
@@ -4,6 +4,7 @@
 <input type="hidden" name="{{widget.name}}$latlng" {% if widget.value %}value="{{widget.value}}"{% endif %}>
 <div id="map-{{widget.name}}" class="qommon-map"
   {% if widget.readonly %}data-readonly="true"{% endif %}
+  {% if widget.sync_map_and_address_fields %}data-address-sync="true"{% endif %}
   {% for key, value in widget.map_attributes.items %}{{key}}="{{value}}" {% endfor %}
   {% if widget.initial_position %}
     data-init-lat="{{ widget.initial_position.lat }}"
diff --git a/wcs/urls.py b/wcs/urls.py
index 6eebcafdb..d359e13d8 100644
--- a/wcs/urls.py
+++ b/wcs/urls.py
@@ -27,6 +27,7 @@ urlpatterns = [
     url(r'^api/validate-condition$', api.validate_condition, name='api-validate-condition'),
     url(r'^api/validate-expression$', api.validate_expression, name='api-validate-expression'),
     url(r'^api/reverse-geocoding$', api.reverse_geocoding, name='api-reverse-geocoding'),
+    url(r'^api/geocoding$', api.geocoding, name='api-geocoding'),
 
     # provide django.contrib.auth view names for compatibility with
     # templates created for classic django applications.
-- 
2.20.1

