Projet

Général

Profil

0003-workflows-add-action-to-geolocate-a-formdata-10581.patch

Frédéric Péters, 01 mai 2016 11:46

Télécharger (18,8 ko)

Voir les différences:

Subject: [PATCH 3/4] workflows: add action to geolocate a formdata (#10581)

 help/fr/wf-geolocate.page      |  70 ++++++++++++++++
 tests/image-with-gps-data.jpeg | Bin 0 -> 1834 bytes
 tests/test_workflows.py        | 163 +++++++++++++++++++++++++++++++++++-
 wcs/wf/geolocate.py            | 182 +++++++++++++++++++++++++++++++++++++++++
 wcs/workflows.py               |   1 +
 5 files changed, 415 insertions(+), 1 deletion(-)
 create mode 100644 help/fr/wf-geolocate.page
 create mode 100644 tests/image-with-gps-data.jpeg
 create mode 100644 wcs/wf/geolocate.py
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
-