0003-workflows-add-action-to-geolocate-a-formdata-10581.patch
help/fr/wf-geolocate.page | ||
---|---|---|
1 |
<page xmlns="http://projectmallard.org/1.0/" |
|
2 |
type="topic" id="wf-geolocate" xml:lang="fr"> |
|
3 | ||
4 |
<info> |
|
5 |
<link type="guide" xref="index#wf" /> |
|
6 |
<revision docversion="0.1" date="2016-05-01" status="draft"/> |
|
7 |
<credit type="author"> |
|
8 |
<name>Frédéric Péters</name> |
|
9 |
<email>fpeters@entrouvert.com</email> |
|
10 |
</credit> |
|
11 | ||
12 |
</info> |
|
13 | ||
14 |
<title>Géolocalisation</title> |
|
15 | ||
16 |
<p> |
|
17 |
Une fois la géolocalisation activée pour un formulaire, le workflow associé |
|
18 |
peut faire appel à l'action de géolocalisation pour attacher des coordonnées |
|
19 |
géographiques à la demande. |
|
20 |
</p> |
|
21 | ||
22 |
<p> |
|
23 |
Ces coordonnées peuvent être obtenues par géocodage à partir d'une adresse ou |
|
24 |
en les extrayant d'un champ « Carte » ou des métadonnées attachées à une |
|
25 |
photographie qui aurait été transférée via un champ de type « Fichier ». |
|
26 |
</p> |
|
27 | ||
28 |
<p> |
|
29 |
Si les coordonnées peuvent être tirées de différentes sources, il est possible |
|
30 |
de faire se succéder plusieurs appels à une action de géolocalisation, en les |
|
31 |
paramétrant pour ne pas écraser des coordonnées précédemment acquises. |
|
32 |
</p> |
|
33 | ||
34 |
<section> |
|
35 |
<title>Géocodage à partir d'une adresse</title> |
|
36 | ||
37 |
<p> |
|
38 |
Le paramétrage se fait en renseignant une chaîne de caractère produisant une |
|
39 |
adresse, généralement en utilisant le mécanisme de substitution pour |
|
40 |
concaténer plusieurs champs. |
|
41 |
</p> |
|
42 | ||
43 |
<example><code>[form_var_numero] [form_var_voie], [form_var_commune]</code></example> |
|
44 |
</section> |
|
45 | ||
46 |
<section> |
|
47 |
<title>Extraction d'un champ « Carte »</title> |
|
48 | ||
49 |
<p> |
|
50 |
Le paramètre est une expression faisant référence à une variable tirée d'un |
|
51 |
champ « Carte ». |
|
52 |
</p> |
|
53 | ||
54 |
<example><code>=form_var_carte</code></example> |
|
55 |
</section> |
|
56 | ||
57 |
<section> |
|
58 |
<title>Extraction d'une photographie</title> |
|
59 | ||
60 |
<p> |
|
61 |
Le paramètre est une expression pointant une variable tirée d'un champ de |
|
62 |
type « Fichier »; le fichier ainsi pointé doit être une image contenant des |
|
63 |
métadonnées EXIF, renseignant la localisation de la prise de vue. |
|
64 |
</p> |
|
65 | ||
66 |
<example><code>=form_var_photo</code></example> |
|
67 |
</section> |
|
68 | ||
69 |
</page> |
|
70 |
tests/test_workflows.py | ||
---|---|---|
2 | 2 |
import pytest |
3 | 3 |
import shutil |
4 | 4 |
import time |
5 |
import urllib2 |
|
6 | ||
7 |
import mock |
|
5 | 8 | |
6 | 9 |
from quixote import cleanup, get_response |
7 | 10 |
from wcs.qommon.http_request import HTTPRequest |
... | ... | |
9 | 12 | |
10 | 13 |
from wcs.formdef import FormDef |
11 | 14 |
from wcs import sessions |
12 |
from wcs.fields import StringField, DateField |
|
15 |
from wcs.fields import StringField, DateField, MapField, FileField
|
|
13 | 16 |
from wcs.roles import Role |
14 | 17 |
from wcs.workflows import (Workflow, WorkflowStatusItem, |
15 | 18 |
SendmailWorkflowStatusItem, SendSMSWorkflowStatusItem, |
... | ... | |
27 | 30 |
from wcs.wf.roles import AddRoleWorkflowStatusItem, RemoveRoleWorkflowStatusItem |
28 | 31 |
from wcs.wf.wscall import WebserviceCallStatusItem |
29 | 32 |
from wcs.wf.export_to_model import transform_to_pdf |
33 |
from wcs.wf.geolocate import GeolocateWorkflowStatusItem |
|
30 | 34 | |
31 | 35 |
from utilities import (create_temporary_pub, MockSubstitutionVariables, emails, |
32 | 36 |
http_requests, clean_temporary_pub, sms_mocking) |
... | ... | |
1074 | 1078 |
item.perform(formdata) |
1075 | 1079 |
assert formdata.get_criticality_level_object().name == 'green' |
1076 | 1080 | |
1081 |
def test_geolocate_address(pub): |
|
1082 |
formdef = FormDef() |
|
1083 |
formdef.geolocations = {'base': 'bla'} |
|
1084 |
formdef.name = 'baz' |
|
1085 |
formdef.fields = [ |
|
1086 |
StringField(id='1', label='String', type='string', varname='string'), |
|
1087 |
] |
|
1088 |
formdef.store() |
|
1089 | ||
1090 |
formdata = formdef.data_class()() |
|
1091 |
formdata.data = {'1': '169 rue du chateau'} |
|
1092 |
formdata.just_created() |
|
1093 |
formdata.store() |
|
1094 |
pub.substitutions.feed(formdata) |
|
1095 | ||
1096 |
item = GeolocateWorkflowStatusItem() |
|
1097 |
item.method = 'address_string' |
|
1098 |
item.address_string = '[form_var_string], paris, france' |
|
1099 | ||
1100 |
with mock.patch('wcs.wf.geolocate.http_get_page') as http_get_page: |
|
1101 |
http_get_page.return_value = (None, 200, |
|
1102 |
json.dumps([{'lat':'48.8337085','lon':'2.3233693'}]), None) |
|
1103 |
item.perform(formdata) |
|
1104 |
assert urllib2.quote('169 rue du chateau, paris') in http_get_page.call_args[0][0] |
|
1105 |
assert int(formdata.geolocations['base']['lat']) == 48 |
|
1106 |
assert int(formdata.geolocations['base']['lon']) == 2 |
|
1107 | ||
1108 |
# check for invalid ezt |
|
1109 |
item.address_string = '[if-any], paris, france' |
|
1110 |
formdata.geolocations = None |
|
1111 |
item.perform(formdata) |
|
1112 |
assert formdata.geolocations == {} |
|
1113 | ||
1114 |
# check for nominatim server error |
|
1115 |
formdata.geolocations = None |
|
1116 |
with mock.patch('wcs.wf.geolocate.http_get_page') as http_get_page: |
|
1117 |
http_get_page.return_value = (None, 500, |
|
1118 |
json.dumps([{'lat':'48.8337085','lon':'2.3233693'}]), None) |
|
1119 |
item.perform(formdata) |
|
1120 |
assert formdata.geolocations == {} |
|
1121 | ||
1122 |
# check for nominatim returning an empty result set |
|
1123 |
formdata.geolocations = None |
|
1124 |
with mock.patch('wcs.wf.geolocate.http_get_page') as http_get_page: |
|
1125 |
http_get_page.return_value = (None, 200, json.dumps([]), None) |
|
1126 |
item.perform(formdata) |
|
1127 |
assert formdata.geolocations == {} |
|
1128 | ||
1129 |
def test_geolocate_image(pub): |
|
1130 |
formdef = FormDef() |
|
1131 |
formdef.name = 'baz' |
|
1132 |
formdef.geolocations = {'base': 'bla'} |
|
1133 |
formdef.fields = [ |
|
1134 |
FileField(id='3', label='File', type='file', varname='file'), |
|
1135 |
] |
|
1136 |
formdef.store() |
|
1137 | ||
1138 |
upload = PicklableUpload('test.jpeg', 'image/jpeg') |
|
1139 |
upload.receive([open(os.path.join(os.path.dirname(__file__), 'image-with-gps-data.jpeg')).read()]) |
|
1140 | ||
1141 |
formdata = formdef.data_class()() |
|
1142 |
formdata.data = {'3': upload} |
|
1143 |
formdata.just_created() |
|
1144 |
formdata.store() |
|
1145 |
pub.substitutions.feed(formdata) |
|
1146 | ||
1147 |
item = GeolocateWorkflowStatusItem() |
|
1148 |
item.method = 'photo_variable' |
|
1149 | ||
1150 |
item.photo_variable = '=form_var_file_raw' |
|
1151 |
item.perform(formdata) |
|
1152 |
assert int(formdata.geolocations['base']['lat']) == -1 |
|
1153 |
assert int(formdata.geolocations['base']['lon']) == 6 |
|
1154 | ||
1155 |
# invalid expression |
|
1156 |
formdata.geolocations = None |
|
1157 |
item.photo_variable = '=1/0' |
|
1158 |
item.perform(formdata) |
|
1159 |
assert formdata.geolocations == {} |
|
1160 | ||
1161 |
# invalid type |
|
1162 |
formdata.geolocations = None |
|
1163 |
item.photo_variable = '="bla"' |
|
1164 |
item.perform(formdata) |
|
1165 |
assert formdata.geolocations == {} |
|
1166 | ||
1167 |
# invalid photo |
|
1168 |
upload = PicklableUpload('test.jpeg', 'image/jpeg') |
|
1169 |
upload.receive([open(os.path.join(os.path.dirname(__file__), 'template.odt')).read()]) |
|
1170 |
formdata.data = {'3': upload} |
|
1171 |
formdata.geolocations = None |
|
1172 |
item.perform(formdata) |
|
1173 |
assert formdata.geolocations == {} |
|
1174 | ||
1175 |
def test_geolocate_map(pub): |
|
1176 |
formdef = FormDef() |
|
1177 |
formdef.name = 'baz' |
|
1178 |
formdef.geolocations = {'base': 'bla'} |
|
1179 |
formdef.fields = [ |
|
1180 |
MapField(id='2', label='Map', type='map', varname='map'), |
|
1181 |
] |
|
1182 |
formdef.store() |
|
1183 | ||
1184 |
formdata = formdef.data_class()() |
|
1185 |
formdata.data = {'2': '48.8337085;2.3233693'} |
|
1186 |
formdata.just_created() |
|
1187 |
formdata.store() |
|
1188 |
pub.substitutions.feed(formdata) |
|
1189 | ||
1190 |
item = GeolocateWorkflowStatusItem() |
|
1191 |
item.method = 'map_variable' |
|
1192 |
item.map_variable = '=form_var_map' |
|
1193 | ||
1194 |
item.perform(formdata) |
|
1195 |
assert int(formdata.geolocations['base']['lat']) == 48 |
|
1196 |
assert int(formdata.geolocations['base']['lon']) == 2 |
|
1197 | ||
1198 |
# invalid data |
|
1199 |
formdata.geolocations = None |
|
1200 |
item.map_variable = '=form_var' |
|
1201 |
item.perform(formdata) |
|
1202 |
assert formdata.geolocations == {} |
|
1203 | ||
1204 |
def test_geolocate_overwrite(pub): |
|
1205 |
formdef = FormDef() |
|
1206 |
formdef.name = 'baz' |
|
1207 |
formdef.geolocations = {'base': 'bla'} |
|
1208 |
formdef.fields = [ |
|
1209 |
MapField(id='2', label='Map', type='map', varname='map'), |
|
1210 |
] |
|
1211 |
formdef.store() |
|
1212 | ||
1213 |
formdata = formdef.data_class()() |
|
1214 |
formdata.data = {'2': '48.8337085;2.3233693'} |
|
1215 |
formdata.just_created() |
|
1216 |
formdata.store() |
|
1217 |
pub.substitutions.feed(formdata) |
|
1218 | ||
1219 |
item = GeolocateWorkflowStatusItem() |
|
1220 |
item.method = 'map_variable' |
|
1221 |
item.map_variable = '=form_var_map' |
|
1222 | ||
1223 |
item.perform(formdata) |
|
1224 |
assert int(formdata.geolocations['base']['lat']) == 48 |
|
1225 |
assert int(formdata.geolocations['base']['lon']) == 2 |
|
1226 | ||
1227 |
formdata.data = {'2': '48.8337085;3.3233693'} |
|
1228 |
item.perform(formdata) |
|
1229 |
assert int(formdata.geolocations['base']['lat']) == 48 |
|
1230 |
assert int(formdata.geolocations['base']['lon']) == 3 |
|
1231 | ||
1232 |
formdata.data = {'2': '48.8337085;4.3233693'} |
|
1233 |
item.overwrite = False |
|
1234 |
item.perform(formdata) |
|
1235 |
assert int(formdata.geolocations['base']['lat']) == 48 |
|
1236 |
assert int(formdata.geolocations['base']['lon']) == 3 |
|
1237 | ||
1077 | 1238 |
@pytest.mark.skipif(transform_to_pdf is None, reason='libreoffice not found') |
1078 | 1239 |
def test_transform_to_pdf(): |
1079 | 1240 |
instream = open(os.path.join(os.path.dirname(__file__), 'template.odt')) |
wcs/wf/geolocate.py | ||
---|---|---|
1 |
# w.c.s. - web application for online forms |
|
2 |
# Copyright (C) 2005-2016 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software; you can redistribute it and/or modify |
|
5 |
# it under the terms of the GNU General Public License as published by |
|
6 |
# the Free Software Foundation; either version 2 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU General Public License |
|
15 |
# along with this program; if not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
17 |
import collections |
|
18 |
import json |
|
19 |
import urllib2 |
|
20 | ||
21 |
try: |
|
22 |
from PIL import Image |
|
23 |
from PIL.ExifTags import TAGS, GPSTAGS |
|
24 |
except ImportError: |
|
25 |
Image = None |
|
26 | ||
27 |
from quixote import get_publisher |
|
28 | ||
29 |
from qommon import get_logger |
|
30 |
from qommon.form import RadiobuttonsWidget, StringWidget, CheckboxWidget |
|
31 |
from qommon.misc import http_get_page |
|
32 |
from wcs.workflows import (WorkflowStatusItem, register_item_class, |
|
33 |
template_on_formdata) |
|
34 | ||
35 |
class GeolocateWorkflowStatusItem(WorkflowStatusItem): |
|
36 |
description = N_('Geolocate') |
|
37 |
key = 'geolocate' |
|
38 | ||
39 |
method = 'address_string' |
|
40 |
address_string = None |
|
41 |
map_variable = None |
|
42 |
photo_variable = None |
|
43 |
overwrite = True |
|
44 | ||
45 |
def get_parameters(self): |
|
46 |
return ('method', 'address_string', 'map_variable', 'photo_variable', 'overwrite') |
|
47 | ||
48 |
def add_parameters_widgets(self, form, parameters, prefix='', formdef=None): |
|
49 |
methods = collections.OrderedDict( |
|
50 |
[('address_string', _('Address String')), |
|
51 |
('map_variable', _('Map Variable')), |
|
52 |
('photo_variable', _('Photo Variable'))]) |
|
53 | ||
54 |
if Image is None: |
|
55 |
del methods['photo_variable'] |
|
56 | ||
57 |
if 'method' in parameters: |
|
58 |
form.add(RadiobuttonsWidget, '%smethod' % prefix, |
|
59 |
title=_('Method'), |
|
60 |
options=methods.items(), |
|
61 |
value=self.method, |
|
62 |
attrs={'data-dynamic-display-parent': 'true'}) |
|
63 |
if 'address_string' in parameters: |
|
64 |
form.add(StringWidget, '%saddress_string' % prefix, size=50, |
|
65 |
title=_('Address String'), value=self.address_string, |
|
66 |
attrs={ |
|
67 |
'data-dynamic-display-child-of': '%smethod' % prefix, |
|
68 |
'data-dynamic-display-value': methods.get('address_string'), |
|
69 |
}) |
|
70 |
if 'map_variable' in parameters: |
|
71 |
form.add(StringWidget, '%smap_variable' % prefix, size=50, |
|
72 |
title=_('Map Variable'), value=self.map_variable, |
|
73 |
attrs={ |
|
74 |
'data-dynamic-display-child-of': '%smethod' % prefix, |
|
75 |
'data-dynamic-display-value': methods.get('map_variable'), |
|
76 |
}) |
|
77 |
if 'photo_variable' in parameters: |
|
78 |
form.add(StringWidget, '%sphoto_variable' % prefix, size=50, |
|
79 |
title=_('Photo Variable'), value=self.photo_variable, |
|
80 |
attrs={ |
|
81 |
'data-dynamic-display-child-of': '%smethod' % prefix, |
|
82 |
'data-dynamic-display-value': methods.get('photo_variable'), |
|
83 |
}) |
|
84 |
if 'overwrite' in parameters: |
|
85 |
form.add(CheckboxWidget, '%soverwrite' % prefix, |
|
86 |
title=_('Overwrite existing geolocation'), |
|
87 |
value=self.overwrite) |
|
88 | ||
89 |
def perform(self, formdata): |
|
90 |
if not self.method: |
|
91 |
return |
|
92 |
if not formdata.formdef.geolocations: |
|
93 |
return |
|
94 |
geolocation_point = formdata.formdef.geolocations.keys()[0] |
|
95 |
if not formdata.geolocations: |
|
96 |
formdata.geolocations = {} |
|
97 |
if formdata.geolocations.get(geolocation_point) and not self.overwrite: |
|
98 |
return |
|
99 |
location = getattr(self, 'geolocate_' + self.method)(formdata) |
|
100 |
if location: |
|
101 |
formdata.geolocations[geolocation_point] = location |
|
102 |
formdata.store() |
|
103 | ||
104 |
def geolocate_address_string(self, formdata): |
|
105 |
nominatim_url = get_publisher().get_site_option('nominatim_url') |
|
106 |
if not nominatim_url: |
|
107 |
nominatim_url = 'http://nominatim.openstreetmap.org' |
|
108 | ||
109 |
try: |
|
110 |
address = template_on_formdata(formdata, self.address_string) |
|
111 |
except Exception, e: |
|
112 |
get_logger().error('error in template for address string [%r]', e) |
|
113 |
return |
|
114 | ||
115 |
url = '%s/search?q=%s&format=json' % (nominatim_url, urllib2.quote(address)) |
|
116 |
response, status, data, auth_header = http_get_page(url) |
|
117 |
if status != 200: |
|
118 |
get_logger().error('error calling geocoding service [%s]', status) |
|
119 |
return |
|
120 |
data = json.loads(data) |
|
121 |
if len(data) == 0: |
|
122 |
get_logger().error('error finding location') |
|
123 |
return |
|
124 |
coords = data[0] |
|
125 |
return {'lon': float(coords['lon']), 'lat': float(coords['lat'])} |
|
126 | ||
127 |
def geolocate_map_variable(self, formdata): |
|
128 |
value = self.compute(self.map_variable) |
|
129 |
if not value: |
|
130 |
return |
|
131 | ||
132 |
try: |
|
133 |
lat, lon = map(float, value.split(';')) |
|
134 |
except Exception, e: |
|
135 |
get_logger().error('error geolocating from map variable [%r]', e) |
|
136 |
return |
|
137 | ||
138 |
return {'lon': lon, 'lat': lat} |
|
139 | ||
140 |
def geolocate_photo_variable(self, formdata): |
|
141 |
if Image is None: |
|
142 |
get_logger().error('error geolocating from file (missing PIL)') |
|
143 |
return |
|
144 | ||
145 |
value = self.compute(self.photo_variable) |
|
146 |
if not hasattr(value, 'get_file_pointer'): |
|
147 |
get_logger().error('error geolocating from photo, invalid variable') |
|
148 |
return |
|
149 | ||
150 |
try: |
|
151 |
image = Image.open(value.get_file_pointer()) |
|
152 |
except IOError: |
|
153 |
get_logger().error('error geolocating from photo, invalid file') |
|
154 |
return |
|
155 | ||
156 |
exif_data = image._getexif() |
|
157 |
if exif_data: |
|
158 |
gps_info = exif_data.get(0x8825) |
|
159 |
if gps_info: |
|
160 |
# lat_ref will be N/S, lon_ref wil l be E/W |
|
161 |
# lat and lon will be degrees/minutes/seconds (value, denominator), |
|
162 |
# like ((33, 1), (51, 1), (2191, 100)) |
|
163 |
lat, lon = gps_info[2], gps_info[4] |
|
164 |
try: |
|
165 |
lat_ref = gps_info[1] |
|
166 |
except KeyError: |
|
167 |
lat_ref = 'N' |
|
168 |
try: |
|
169 |
lon_ref = gps_info[3] |
|
170 |
except KeyError: |
|
171 |
lon_ref = 'E' |
|
172 |
lat = (1.0*lat[0][0]/lat[0][1] + 1.0*lat[1][0]/lat[1][1]/60 + 1.0*lat[2][0]/lat[2][1]/3600) |
|
173 |
lon = (1.0*lon[0][0]/lon[0][1] + 1.0*lon[1][0]/lon[1][1]/60 + 1.0*lon[2][0]/lon[2][1]/3600) |
|
174 |
if lat_ref == 'S': |
|
175 |
lat = -lat |
|
176 |
if lon_ref == 'W': |
|
177 |
lon = -lon |
|
178 |
return {'lon': lon, 'lat': lat} |
|
179 |
return |
|
180 | ||
181 | ||
182 |
register_item_class(GeolocateWorkflowStatusItem) |
wcs/workflows.py | ||
---|---|---|
2223 | 2223 |
import wf.remove |
2224 | 2224 |
import wf.roles |
2225 | 2225 |
import wf.dispatch |
2226 |
import wf.geolocate |
|
2226 | 2227 |
import wf.wscall |
2227 | 2228 |
import wf.form |
2228 | 2229 |
import wf.register_comment |
2229 |
- |