From e4da80dac32828eabfe764ced5c9e4e33c0db44b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20P=C3=A9ters?= Date: Tue, 4 Nov 2014 10:39:35 +0100 Subject: [PATCH] geolocation prefill, with reverse geocoding (#5855) --- wcs/api.py | 13 ++++++- wcs/fields.py | 32 +++++++++++++++- wcs/forms/root.py | 1 + wcs/qommon/form.py | 17 ++++++--- wcs/qommon/http_response.py | 4 ++ wcs/qommon/static/css/qommon.css | 5 +++ wcs/qommon/static/js/qommon.geolocation.js | 31 +++++++++++++++ wcs/qommon/static/js/qommon.map.js | 61 +++++++++++++++++++----------- wcs/qommon/template.py | 2 + wcs/root.py | 5 ++- 10 files changed, 138 insertions(+), 33 deletions(-) create mode 100644 wcs/qommon/static/js/qommon.geolocation.js diff --git a/wcs/api.py b/wcs/api.py index 5d25e25..f589d3b 100644 --- a/wcs/api.py +++ b/wcs/api.py @@ -18,8 +18,10 @@ import base64 import hmac import hashlib import datetime +import urllib2 -from quixote import get_request, get_publisher +from quixote import get_request, get_publisher, get_response +from quixote.directory import Directory from qommon.errors import AccessForbiddenError def get_user_from_api_query_string(): @@ -81,3 +83,12 @@ def get_user_from_api_query_string(): raise AccessForbiddenError('missing email or NameID fields') return user + + +class ApiDirectory(Directory): + _q_exports = [('reverse-geocoding', 'reverse_geocoding')] + + def reverse_geocoding(self): + get_response().set_content_type('application/json') + return urllib2.urlopen('http://nominatim.openstreetmap.org/reverse?format=json&zoom=18&addressdetails=1&'\ + + 'lat=%s&lon=%s' % (get_request().form['lat'], get_request().form['lon'])).read() diff --git a/wcs/fields.py b/wcs/fields.py index ac473f1..ace9f4e 100644 --- a/wcs/fields.py +++ b/wcs/fields.py @@ -47,7 +47,11 @@ class PrefillSelectionWidget(CompositeWidget): options = [('none', _('None')), ('string', _('String')), ('formula', _('Formula (Python)')), - ('user', _('User Field'))] + ('user', _('User Field')), + ('geolocation', _('Geolocation')),] + + if kwargs.get('map'): + options = [('none', _('None')), ('geolocation', _('Geolocation')),] self.add(SingleSelectWidget, 'type', options = options, value = value.get('type')) @@ -74,6 +78,19 @@ class PrefillSelectionWidget(CompositeWidget): self.add(SingleSelectWidget, 'value', value = value.get('value'), options = user_fields) + elif self.value.get('type') == 'geolocation': + if kwargs.get('map'): + geoloc_fields = [('position', _('Position'))] + else: + geoloc_fields = [ + ('house', _('Number')), + ('road', _('Street')), + ('street-and-no', _('Street and number')), + ('city', _('City')), + ('country', _('Country')), + ] + self.add(SingleSelectWidget, 'value', value=value.get('value'), + options=geoloc_fields) self.add(SubmitWidget, 'apply', value = _('Apply')) @@ -247,6 +264,15 @@ class Field: except: pass + elif t == 'geolocation': + return None + + return None + + def get_prefill_attributes(self): + t = self.prefill.get('type') + if t == 'geolocation': + return {'geolocation': self.prefill.get('value')} return None def feed_session(self, value, display_value): @@ -259,6 +285,7 @@ class WidgetField(Field): in_listing = True extra_attributes = [] prefill = {} + prefill_kwargs = {} def add_to_form(self, form, value = None): kwargs = {'required': self.required} @@ -319,7 +346,7 @@ class WidgetField(Field): form.add(StringWidget, 'extra_css_class', title = _('Extra class for CSS styling'), value = self.extra_css_class, size = 30) form.add(PrefillSelectionWidget, 'prefill', title = _('Prefill'), - value = self.prefill) + value=self.prefill, **self.prefill_kwargs) def check_admin_form(self, form): return @@ -1376,6 +1403,7 @@ class MapField(WidgetField): widget_class = MapWidget extra_attributes = ['initial_zoom', 'min_zoom', 'max_zoom', 'default_position', 'init_with_geoloc'] + prefill_kwargs = {'map': True} def fill_admin_form(self, form): WidgetField.fill_admin_form(self, form) diff --git a/wcs/forms/root.py b/wcs/forms/root.py index 0b94b53..a35c409 100644 --- a/wcs/forms/root.py +++ b/wcs/forms/root.py @@ -259,6 +259,7 @@ class FormPage(Directory): form.get_widget('f%s' % k).set_message( _('Value has been automatically prefilled.')) form.get_widget('f%s' % k).prefilled = True + form.get_widget('f%s' % k).prefill_attributes = field.get_prefill_attributes() if not prefilled and form.get_widget('f%s' % k): form.get_widget('f%s' % k).clear_error() diff --git a/wcs/qommon/form.py b/wcs/qommon/form.py index 2c98ded..e0f5ac0 100644 --- a/wcs/qommon/form.py +++ b/wcs/qommon/form.py @@ -110,11 +110,16 @@ def render(self): classnames += ' widget-required' if self.is_prefilled(): classnames += ' widget-prefilled' + attributes = {} if hasattr(self, 'div_id') and self.div_id: - r += htmltext('
') \ - % (classnames, self.div_id, self.name) - else: - r += htmltext('
') % classnames + attributes['data-valuecontainerid'] = 'form_%s' % self.name + if hasattr(self, 'prefill_attributes') and self.prefill_attributes: + for k, v in self.prefill_attributes.items(): + attributes['data-' + k] = v + if hasattr(self, 'div_id') and self.div_id: + attributes['id'] = self.div_id + attributes['class'] = classnames + r += htmltext('
' % ' '.join(['%s="%s"' % x for x in attributes.items()])) r += self.render_title(self.get_title()) classnames = 'content' if hasattr(self, 'content_extra_css_class') and self.content_extra_css_class: @@ -1900,12 +1905,12 @@ class MapWidget(CompositeWidget): if self.readonly: attrs['data-readonly'] = 'true' for attribute in ('initial_zoom', 'min_zoom', 'max_zoom'): - if attribute in self.kwargs: + if attribute in self.kwargs and self.kwargs.get(attribute) is not None: attrs['data-%s' % attribute] = self.kwargs.get(attribute) if self.kwargs.get('default_position'): attrs['data-def-lat'], attrs['data-def-lng'] = self.kwargs.get('default_position').split(';') if self.kwargs.get('init_with_geoloc'): - attrs['data-init-with-geologc'] = 1 + attrs['data-init-with-geoloc'] = 1 r += htmltext('
' % ' '.join(['%s="%s"' % x for x in attrs.items()])) return r.getvalue() diff --git a/wcs/qommon/http_response.py b/wcs/qommon/http_response.py index a13bcb9..59d2bff 100644 --- a/wcs/qommon/http_response.py +++ b/wcs/qommon/http_response.py @@ -71,6 +71,10 @@ class HTTPResponse(quixote.http_response.HTTPResponse): get_publisher().get_application_static_files_root_url()) if script_name == 'popup.js': self.add_css_include('../js/smoothness/jquery-ui-1.10.0.custom.min.css') + if script_name == 'qommon.geolocation.js': + self.add_javascript(['jquery.js']) + self.add_javascript_code('var WCS_ROOT_URL = "%s";\n' % \ + get_publisher().get_frontoffice_url()) def add_javascript_code(self, code): if not self.javascript_code_parts: diff --git a/wcs/qommon/static/css/qommon.css b/wcs/qommon/static/css/qommon.css index c6373e0..7cd86a0 100644 --- a/wcs/qommon/static/css/qommon.css +++ b/wcs/qommon/static/css/qommon.css @@ -393,3 +393,8 @@ ul.select2-results { div.qommon-map { height: 280px; } + +label.activity { + background: url(indicator.gif) no-repeat top right; + padding-right: 30px; +} diff --git a/wcs/qommon/static/js/qommon.geolocation.js b/wcs/qommon/static/js/qommon.geolocation.js new file mode 100644 index 0000000..dc5b92d --- /dev/null +++ b/wcs/qommon/static/js/qommon.geolocation.js @@ -0,0 +1,31 @@ +$(function() { + $(document).on('set-geolocation', function(event, coords) { + $.getJSON(WCS_ROOT_URL + '/api/reverse-geocoding?lat=' + coords.lat + '&lon=' + coords.lng, function(data) { + $('div[data-geolocation="house"] input').val(data.address.house_number); + $('div[data-geolocation="road"] input').val(data.address.road); + if (data.address.road && data.address.house_number) { + street_and_number = data.address.road + ' ' + data.address.house_number; + } else { + street_and_number = data.address.road; + } + $('div[data-geolocation="street-and-no"] input').val(street_and_number); + $('div[data-geolocation="city"] input').val(data.address.city || data.address.county); + $('div[data-geolocation="country"] input').val(data.address.country); + }); + }); + if ($('.qommon-map').length == 0) { + /* if there's no map on the page, we do the geolocation without leaflet. */ + if (navigator.geolocation) { + $('div[data-geolocation] label').addClass('activity'); + navigator.geolocation.getCurrentPosition( + function (position) { + $('div[data-geolocation] label').removeClass('activity'); + var coords = {lat: position.coords.latitude, lng: position.coords.longitude}; + $(document).trigger('set-geolocation', coords); + }, + function (error_msg) { + $('div[data-geolocation] label').removeClass('activity'); + }); + } + } +}); diff --git a/wcs/qommon/static/js/qommon.map.js b/wcs/qommon/static/js/qommon.map.js index 7849cc7..37fc377 100644 --- a/wcs/qommon/static/js/qommon.map.js +++ b/wcs/qommon/static/js/qommon.map.js @@ -1,27 +1,28 @@ $(function() { $('.qommon-map').each(function() { + var $map_widget = $(this); var map_options = Object(); - var initial_zoom = parseInt($(this).data('initial_zoom')); + var initial_zoom = parseInt($map_widget.data('initial_zoom')); if (! isNaN(initial_zoom)) { map_options.zoom = initial_zoom; } else { map_options.zoom = 13; } - var max_zoom = parseInt($(this).data('max_zoom')); + var max_zoom = parseInt($map_widget.data('max_zoom')); if (! isNaN(max_zoom)) map_options.maxZoom = max_zoom; - var min_zoom = parseInt($(this).data('min_zoom')); + var min_zoom = parseInt($map_widget.data('min_zoom')); if (! isNaN(min_zoom)) map_options.minZoom = min_zoom; - var map = L.map($(this).attr('id'), map_options); + var map = L.map($map_widget.attr('id'), map_options); this.map_object = map; - var hidden = $(this).prev(); + var hidden = $map_widget.prev(); map.marker = null; var latlng; - if ($(this).data('init-lat')) { - latlng = [$(this).data('init-lat'), $(this).data('init-lng')] + 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); - } else if ($(this).data('def-lat')) { - latlng = [$(this).data('def-lat'), $(this).data('def-lng')] + } else if ($map_widget.data('def-lat')) { + latlng = [$map_widget.data('def-lat'), $map_widget.data('def-lng')] } else { latlng = [50.84, 4.36]; } @@ -31,25 +32,41 @@ $(function() { { attribution: 'Map data © OpenStreetMap contributors, CC-BY-SA' }).addTo(map); - if (! $(this).data('readonly')) { + if (! $map_widget.data('readonly')) { map.on('click', function(e) { - if (map.marker === null) { - map.marker = L.marker([0, 0]); - map.marker.addTo(map); - } - map.marker.setLatLng(e.latlng); - hidden.val(e.latlng.lat + ';' + e.latlng.lng); + $map_widget.trigger('set-geolocation', e.latlng); }); } - if ($(this).data('init-with-geoloc')) { + $map_widget.on('set-geolocation', function(e, coords) { + if (map.marker === null) { + map.marker = L.marker([0, 0]); + map.marker.addTo(map); + } + map.marker.setLatLng(coords); + hidden.val(coords.lat + ';' + coords.lng); + }); + position_prefil = $map_widget.parent().parent().data('geolocation') == 'position'; + if (! $map_widget.data('readonly') && ($map_widget.data('init-with-geoloc') || position_prefil)) { map.on('locationfound', function(e) { - hidden.val(e.latlng.lat + ';' + e.latlng.lng); - map.setView(e.latlng, map_options.zoom); + $map_widget.parent().parent().find('label').removeClass('activity'); + if (map.marker === null) { + hidden.val(e.latlng.lat + ';' + e.latlng.lng); + map.setView(e.latlng, map_options.zoom); + if (position_prefil) { + map.setView(e.latlng, 16); + $map_widget.trigger('set-geolocation', e.latlng); + } + } + }); + map.on('locationerror', function(e) { + $map_widget.parent().parent().find('label').removeClass('activity'); + $map_widget.parent().parent().find('label').after('' + e.message + ''); }); - map.locate({timeout: 1000, maximumAge: 60000}); + $map_widget.parent().parent().find('label').addClass('activity') + map.locate({timeout: 10000, maximumAge: 60000}); } - if ($(this).data('geojson-url')) { - $.getJSON($(this).data('geojson-url'), function(data) { + if ($map_widget.data('geojson-url')) { + $.getJSON($map_widget.data('geojson-url'), function(data) { L.geoJson(data, { onEachFeature: function(feature, layer) { layer.on('click', function() { diff --git a/wcs/qommon/template.py b/wcs/qommon/template.py index c1302c9..0ede665 100644 --- a/wcs/qommon/template.py +++ b/wcs/qommon/template.py @@ -294,6 +294,8 @@ def decorate(body, response): if 'rel="popup"' in body or 'rel="popup"' in kwargs.get('sidebar', ''): response.add_javascript(['jquery.js', 'jquery-ui.js', 'popup.js', 'widget_list.js']) + if 'data-geolocation' in body: + response.add_javascript(['qommon.geolocation.js']) onload = kwargs.get('onload') org_name = get_cfg('sp', {}).get('organization_name', diff --git a/wcs/root.py b/wcs/root.py index eb047b2..ecf28cc 100644 --- a/wcs/root.py +++ b/wcs/root.py @@ -46,7 +46,7 @@ from categories import Category from formdef import FormDef from anonylink import AnonymityLink from roles import Role -from wcs.api import get_user_from_api_query_string +from wcs.api import get_user_from_api_query_string, ApiDirectory from myspace import MyspaceDirectory @@ -190,8 +190,9 @@ class RegisterDirectory(Directory): class RootDirectory(Directory): _q_exports = ['admin', 'backoffice', 'forms', 'login', 'logout', 'liberty', 'token', 'saml', 'ident', 'register', 'afterjobs', 'themes', 'myspace', 'user', 'roles', - 'pages', ('tmp-upload', 'tmp_upload'), '__version__'] + 'pages', ('tmp-upload', 'tmp_upload'), 'api', '__version__'] + api = ApiDirectory() themes = template.ThemesDirectory() myspace = MyspaceDirectory() pages = qommon.pages.PagesDirectory() -- 2.1.1